« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/things/composite.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/composite.js')
-rw-r--r--src/data/things/composite.js1179
1 files changed, 1179 insertions, 0 deletions
diff --git a/src/data/things/composite.js b/src/data/things/composite.js
new file mode 100644
index 00000000..1be60cd1
--- /dev/null
+++ b/src/data/things/composite.js
@@ -0,0 +1,1179 @@
+// Composes multiple compositional "steps" and a "base" to form a property
+// descriptor out of modular building blocks. This is an extension to the
+// more general-purpose CacheableObject property descriptor syntax, and
+// aims to make modular data processing - which lends to declarativity -
+// much easier, without fundamentally altering much of the typical syntax
+// or terminology, nor building on it to an excessive degree.
+//
+// Think of a composition as being a chain of steps which lead into a final
+// base property, which is usually responsible for returning the value that
+// will actually get exposed when the property being described is accessed.
+//
+// == The compositional base: ==
+//
+// The final item in a compositional list is its base, and it identifies
+// the essential qualities of the property descriptor. The compositional
+// steps preceding it may exit early, in which case the expose function
+// defined on the base won't be called; or they will provide dependencies
+// that the base may use to compute the final value that gets exposed for
+// this property.
+//
+// The base indicates the capabilities of the composition as a whole.
+// It should be {expose: true}, since that's the only area that preceding
+// compositional steps (currently) can actually influence. If it's also
+// {update: true}, then the composition as a whole accepts an update value
+// just like normal update-flag property descriptors - meaning it can be
+// set with `thing.someProperty = value` and that value will be paseed
+// into each (implementing) step's transform() function, as well as the
+// base. Bases usually aren't {compose: true}, but can be - check out the
+// section on "nesting compositions" for details about that.
+//
+// Every composition always has exactly one compositional base, and it's
+// always the last item in the composition list. All items preceding it
+// are compositional steps, described below.
+//
+// == Compositional steps: ==
+//
+// Compositional steps are, in essence, typical property descriptors with
+// the extra flag {compose: true}. They operate on existing dependencies,
+// and are typically dynamically constructed by "utility" functions (but
+// can also be manually declared within the step list of a composition).
+// Compositional steps serve two purposes:
+//
+//  1. exit early, if some condition is matched, returning and exposing
+//     some value directly from that step instead of continuing further
+//     down the step list;
+//
+//  2. and/or provide new, dynamically created "private" dependencies which
+//     can be accessed by further steps down the list, or at the base at
+//     the bottom, modularly supplying information that will contribute to
+//     the final value exposed for this property.
+//
+// Usually it's just one of those two, but it's fine for a step to perform
+// both jobs if the situation benefits.
+//
+// Compositional steps are the real "modular" or "compositional" part of
+// this data processing style - they're designed to be combined together
+// in dynamic, versatile ways, as each property demands it. You usually
+// define a compositional step to be returned by some ordinary static
+// property-descriptor-returning function (customarily namespaced under
+// the relevant Thing class's static `composite` field) - that lets you
+// reuse it in multiple compositions later on.
+//
+// Compositional steps are implemented with "continuation passing style",
+// meaning the connection to the next link on the chain is passed right to
+// each step's compute (or transform) function, and the implementation gets
+// to decide whether to continue on that chain or exit early by returning
+// some other value.
+//
+// Every step along the chain, apart from the base at the bottom, has to
+// have the {compose: true} step. That means its compute() or transform()
+// function will be passed an extra argument at the end, `continuation`.
+// To provide new dependencies to items further down the chain, just pass
+// them directly to this continuation() function, customarily with a hash
+// ('#') prefixing each name - for example:
+//
+//   compute({..some dependencies..}, continuation) {
+//     return continuation({
+//       '#excitingProperty': (..a value made from dependencies..),
+//     });
+//   }
+//
+// Performing an early exit is as simple as returning some other value,
+// instead of the continuation. You may also use `continuation.exit(value)`
+// to perform the exact same kind of early exit - it's just a different
+// syntax that might fit in better in certain longer compositions.
+//
+// It may be fine to simply provide new dependencies under a hard-coded
+// name, such as '#excitingProperty' above, but if you're writing a utility
+// that dynamically returns the compositional step and you suspect you
+// might want to use this step multiple times in a single composition,
+// it's customary to accept a name for the result.
+//
+// Here's a detailed example showing off early exit, dynamically operating
+// on a provided dependency name, and then providing a result in another
+// also-provided dependency name:
+//
+//   static Thing.composite.withResolvedContribs = ({
+//     from: contribsByRefDependency,
+//     to: outputDependency,
+//   }) => ({
+//     flags: {expose: true, compose: true},
+//     expose: {
+//       dependencies: [contribsByRefDependency, 'artistData'],
+//       compute({
+//         [contribsByRefDependency]: contribsByRef,
+//         artistData,
+//       }, continuation) {
+//         if (!artistData) return null;  /* early exit! */
+//         return continuation({
+//           [outputDependency]:  /* this is the important part */
+//             (..resolve contributions one way or another..),
+//         });
+//       },
+//     },
+//   });
+//
+// And how you might work that into a composition:
+//
+//   static Track[Thing.getPropertyDescriptors].coverArtists =
+//     Thing.composite.from([
+//       Track.composite.doSomethingWhichMightEarlyExit(),
+//       Thing.composite.withResolvedContribs({
+//         from: 'coverArtistContribsByRef',
+//         to: '#coverArtistContribs',
+//       }),
+//
+//       {
+//         flags: {expose: true},
+//         expose: {
+//           dependencies: ['#coverArtistContribs'],
+//           compute({'#coverArtistContribs': coverArtistContribs}) {
+//             return coverArtistContribs.map(({who}) => who);
+//           },
+//         },
+//       },
+//     ]);
+//
+// One last note! A super common code pattern when creating more complex
+// compositions is to have several steps which *only* expose and compose.
+// As a syntax shortcut, you can skip the outer section. It's basically
+// like writing out just the {expose: {...}} part. Remember that this
+// indicates that the step you're defining is compositional, so you have
+// to specify the flags manually for the base, even if this property isn't
+// going to get an {update: true} flag.
+//
+// == Cache-safe dependency names: ==
+//
+// [Disclosure: The caching engine hasn't actually been implemented yet.
+//  As such, this section is subject to change, and simply provides sound
+//  forward-facing advice and interfaces.]
+//
+// It's a good idea to write individual compositional steps in such a way
+// that they're "cache-safe" - meaning the same input (dependency) values
+// will always result in the same output (continuation or early exit).
+//
+// In order to facilitate this, compositional step descriptors may specify
+// unique `mapDependencies`, `mapContinuation`, and `options` values.
+//
+// Consider the `withResolvedContribs` example adjusted to make use of
+// two of these options below:
+//
+//   static Thing.composite.withResolvedContribs = ({
+//     from: contribsByRefDependency,
+//     to: outputDependency,
+//   }) => ({
+//     flags: {expose: true, compose: true},
+//     expose: {
+//       dependencies: ['artistData'],
+//       mapDependencies: {contribsByRef: contribsByRefDependency},
+//       mapContinuation: {outputDependency},
+//       compute({
+//         contribsByRef, /* no longer in square brackets */
+//         artistData,
+//       }, continuation) {
+//         if (!artistData) return null;
+//         return continuation({
+//           outputDependency: /* no longer in square brackets */
+//             (..resolve contributions one way or another..),
+//         });
+//       },
+//     },
+//   });
+//
+// With a little destructuring and restructuring JavaScript sugar, the
+// above can be simplified some more:
+//
+//   static Thing.composite.withResolvedContribs = ({from, to}) => ({
+//     flags: {expose: true, compose: true},
+//     expose: {
+//       dependencies: ['artistData'],
+//       mapDependencies: {from},
+//       mapContinuation: {to},
+//       compute({artistData, from: contribsByRef}, continuation) {
+//         if (!artistData) return null;
+//         return continuation({
+//           to: (..resolve contributions one way or another..),
+//         });
+//       },
+//     },
+//   });
+//
+// These two properties let you separate the name-mapping behavior (for
+// dependencies and the continuation) from the main body of the compute
+// function. That means the compute function will *always* get inputs in
+// the same form (dependencies 'artistData' and 'from' above), and will
+// *always* provide its output in the same form (early return or 'to').
+//
+// Thanks to that, this `compute` function is cache-safe! Its outputs can
+// be cached corresponding to each set of mapped inputs. So it won't matter
+// whether the `from` dependency is named `coverArtistContribsByRef` or
+// `contributorContribsByRef` or something else - the compute function
+// doesn't care, and only expects that value to be provided via its `from`
+// argument. Likewise, it doesn't matter if the output should be sent to
+// '#coverArtistContribs` or `#contributorContribs` or some other name;
+// the mapping is handled automatically outside, and compute will always
+// output its value to the continuation's `to`.
+//
+// Note that `mapDependencies` and `mapContinuation` should be objects of
+// the same "shape" each run - that is, the values will change depending on
+// outside context, but the keys are always the same. You shouldn't use
+// `mapDependencies` to dynamically select more or fewer dependencies.
+// If you need to dynamically select a range of dependencies, just specify
+// them in the `dependencies` array like usual. The caching engine will
+// understand that differently named `dependencies` indicate separate
+// input-output caches should be used.
+//
+// The 'options' property makes it possible to specify external arguments
+// that fundamentally change the behavior of the `compute` function, while
+// still remaining cache-safe. It indicates that the caching engine should
+// use a completely different input-to-output cache for each permutation
+// of the 'options' values. This way, those functions are still cacheable
+// at all; they'll just be cached separately for each set of option values.
+// Values on the 'options' property will always be provided in compute's
+// dependencies under '#options' (to avoid name conflicts with other
+// dependencies).
+//
+// == To compute or to transform: ==
+//
+// A compositional step can work directly on a property's stored update
+// value, transforming it in place and either early exiting with it or
+// passing it on (via continuation) to the next item(s) in the
+// compositional step list. (If needed, these can provide dependencies
+// the same way as compute functions too - just pass that object after
+// the updated (or same) transform value in your call to continuation().)
+//
+// But in order to make them more versatile, compositional steps have an
+// extra trick up their sleeve. If a compositional step implements compute
+// and *not* transform, it can still be used in a composition targeting a
+// property which updates! These retain their full dependency-providing and
+// early exit functionality - they just won't be provided the update value.
+// If a compute-implementing step returns its continuation, then whichever
+// later step (or the base) next implements transform() will receive the
+// update value that had so far been running - as well as any dependencies
+// the compute() step returned, of course!
+//
+// Please note that a compositional step which transforms *should not*
+// specify, in its flags, {update: true}. Just provide the transform()
+// function in its expose descriptor; it will be automatically detected
+// and used when appropriate.
+//
+// It's actually possible for a step to specify both transform and compute,
+// in which case the transform() implementation will only be selected if
+// the composition's base is {update: true}. It's not exactly known why you
+// would want to specify unique-but-related transform and compute behavior,
+// but the basic possibility was too cool to skip out on.
+//
+// == Nesting compositions: ==
+//
+// Compositional steps are so convenient that you just might want to bundle
+// them together, and form a whole new step-shaped unit of its own!
+//
+// In order to allow for this while helping to ensure internal dependencies
+// remain neatly isolated from the composition which nests your bundle,
+// the Thing.composite.from() function will accept and adapt to a base that
+// specifies the {compose: true} flag, just like the steps preceding it.
+//
+// The continuation function that gets provided to the base will be mildly
+// special - after all, nothing follows the base within the composition's
+// own list! Instead of appending dependencies alongside any previously
+// provided ones to be available to the next step, the base's continuation
+// function should be used to define "exports" of the composition as a
+// whole. It's similar to the usual behavior of the continuation, just
+// expanded to the scope of the composition instead of following steps.
+//
+// For example, suppose your composition (which you expect to include in
+// other compositions) brings about several private, hash-prefixed
+// dependencies to contribute to its own results. Those dependencies won't
+// end up "bleeding" into the dependency list of whichever composition is
+// nesting this one - they will totally disappear once all the steps in
+// the nested composition have finished up.
+//
+// To "export" the results of processing all those dependencies (provided
+// that's something you want to do and this composition isn't used purely
+// for a conditional early-exit), you'll want to define them in the
+// continuation passed to the base. (Customarily, those should start with
+// a hash just like the exports from any other compositional step; they're
+// still dynamically provided dependencies!)
+//
+// Another way to "export" dependencies is by using calling *any* step's
+// `continuation.raise()` function. This is sort of like early exiting,
+// but instead of quitting out the whole entire property, it will just
+// break out of the current, nested composition's list of steps, acting
+// as though the composition had finished naturally. The dependencies
+// passed to `raise` will be the ones which get exported.
+//
+// Since `raise` is another way to export dependencies, if you're using
+// dynamic export names, you should specify `mapContinuation` on the step
+// calling `continuation.raise` as well.
+//
+// An important note on `mapDependencies` here: A nested composition gets
+// free access to all the ordinary properties defined on the thing it's
+// working on, but if you want it to depend on *private* dependencies -
+// ones prefixed with '#' - which were provided by some other compositional
+// step preceding wherever this one gets nested, then you *have* to use
+// `mapDependencies` to gain access. Check out the section on "cache-safe
+// dependency names" for information on this syntax!
+//
+// Also - on rare occasion - you might want to make a reusable composition
+// that itself causes the composition *it's* nested in to raise. If that's
+// the case, give `composition.raiseAbove()` a go! This effectively means
+// kicking out of *two* layers of nested composition - the one including
+// the step with the `raiseAbove` call, and the composition which that one
+// is nested within. You don't need to use `raiseAbove` if the reusable
+// utility function just returns a single compositional step, but if you
+// want to make use of other compositional steps, it gives you access to
+// the same conditional-raise capabilities.
+//
+// Have some syntax sugar! Since nested compositions are defined by having
+// the base be {compose: true}, the composition will infer as much if you
+// don't specifying the base's flags at all. Simply use the same shorthand
+// syntax as for other compositional steps, and it'll work out cleanly!
+//
+
+import {empty, filterProperties, openAggregate} from '#sugar';
+
+import Thing from './thing.js';
+
+export {compositeFrom as from};
+function compositeFrom(firstArg, secondArg) {
+  const debug = fn => {
+    if (compositeFrom.debug === true) {
+      const label =
+        (annotation
+          ? color.dim(`[composite: ${annotation}]`)
+          : color.dim(`[composite]`));
+      const result = fn();
+      if (Array.isArray(result)) {
+        console.log(label, ...result.map(value =>
+          (typeof value === 'object'
+            ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity})
+            : value)));
+      } else {
+        console.log(label, result);
+      }
+    }
+  };
+
+  let annotation, composition;
+  if (typeof firstArg === 'string') {
+    [annotation, composition] = [firstArg, secondArg];
+  } else {
+    [annotation, composition] = [null, firstArg];
+  }
+
+  const base = composition.at(-1);
+  const steps = composition.slice();
+
+  const aggregate = openAggregate({
+    message:
+      `Errors preparing Thing.composite.from() composition` +
+      (annotation ? ` (${annotation})` : ''),
+  });
+
+  const baseExposes =
+    (base.flags
+      ? base.flags.expose
+      : true);
+
+  const baseUpdates =
+    (base.flags
+      ? base.flags.update
+      : false);
+
+  const baseComposes =
+    (base.flags
+      ? base.flags.compose
+      : true);
+
+  if (!baseExposes) {
+    aggregate.push(new TypeError(`All steps, including base, must expose`));
+  }
+
+  const exposeDependencies = new Set();
+
+  let anyStepsCompute = false;
+  let anyStepsTransform = false;
+
+  for (let i = 0; i < steps.length; i++) {
+    const step = steps[i];
+    const isBase = i === steps.length - 1;
+    const message =
+      `Errors in step #${i + 1}` +
+      (isBase ? ` (base)` : ``) +
+      (step.annotation ? ` (${step.annotation})` : ``);
+
+    aggregate.nest({message}, ({push}) => {
+      if (step.flags) {
+        let flagsErrored = false;
+
+        if (!step.flags.compose && !isBase) {
+          push(new TypeError(`All steps but base must compose`));
+          flagsErrored = true;
+        }
+
+        if (!step.flags.expose) {
+          push(new TypeError(`All steps must expose`));
+          flagsErrored = true;
+        }
+
+        if (flagsErrored) {
+          return;
+        }
+      }
+
+      const expose =
+        (step.flags
+          ? step.expose
+          : step);
+
+      const stepComputes = !!expose.compute;
+      const stepTransforms = !!expose.transform;
+
+      if (!stepComputes && !stepTransforms) {
+        push(new TypeError(`Steps must provide compute or transform (or both)`));
+        return;
+      }
+
+      if (
+        stepTransforms && !stepComputes &&
+        !baseUpdates && !baseComposes
+      ) {
+        push(new TypeError(`Steps which only transform can't be composed with a non-updating base`));
+        return;
+      }
+
+      if (stepComputes) {
+        anyStepsCompute = true;
+      }
+
+      if (stepTransforms) {
+        anyStepsTransform = true;
+      }
+
+      // Unmapped dependencies are exposed on the final composition only if
+      // they're "public", i.e. pointing to update values of other properties
+      // on the CacheableObject.
+      for (const dependency of expose.dependencies ?? []) {
+        if (typeof dependency === 'string' && dependency.startsWith('#')) {
+          continue;
+        }
+
+        exposeDependencies.add(dependency);
+      }
+
+      // Mapped dependencies are always exposed on the final composition.
+      // These are explicitly for reading values which are named outside of
+      // the current compositional step.
+      for (const dependency of Object.values(expose.mapDependencies ?? {})) {
+        exposeDependencies.add(dependency);
+      }
+    });
+  }
+
+  if (!baseComposes) {
+    if (baseUpdates) {
+      if (!anyStepsTransform) {
+        push(new TypeError(`Expected at least one step to transform`));
+      }
+    } else {
+      if (!anyStepsCompute) {
+        push(new TypeError(`Expected at least one step to compute`));
+      }
+    }
+  }
+
+  aggregate.close();
+
+  const constructedDescriptor = {};
+
+  if (annotation) {
+    constructedDescriptor.annotation = annotation;
+  }
+
+  constructedDescriptor.flags = {
+    update: baseUpdates,
+    expose: baseExposes,
+    compose: baseComposes,
+  };
+
+  if (baseUpdates) {
+    constructedDescriptor.update = base.update;
+  }
+
+  if (baseExposes) {
+    const expose = constructedDescriptor.expose = {};
+    expose.dependencies = Array.from(exposeDependencies);
+
+    const continuationSymbol = Symbol('continuation symbol');
+    const noTransformSymbol = Symbol('no-transform symbol');
+
+    function _filterDependencies(availableDependencies, {
+      dependencies,
+      mapDependencies,
+      options,
+    }) {
+      const filteredDependencies =
+        (dependencies
+          ? filterProperties(availableDependencies, dependencies)
+          : {});
+
+      if (mapDependencies) {
+        for (const [to, from] of Object.entries(mapDependencies)) {
+          filteredDependencies[to] = availableDependencies[from] ?? null;
+        }
+      }
+
+      if (options) {
+        filteredDependencies['#options'] = options;
+      }
+
+      return filteredDependencies;
+    }
+
+    function _assignDependencies(continuationAssignment, {mapContinuation}) {
+      if (!mapContinuation) {
+        return continuationAssignment;
+      }
+
+      const assignDependencies = {};
+
+      for (const [from, to] of Object.entries(mapContinuation)) {
+        assignDependencies[to] = continuationAssignment[from] ?? null;
+      }
+
+      return assignDependencies;
+    }
+
+    function _prepareContinuation(callingTransformForThisStep) {
+      const continuationStorage = {
+        returnedWith: null,
+        providedDependencies: undefined,
+        providedValue: undefined,
+      };
+
+      const continuation =
+        (callingTransformForThisStep
+          ? (providedValue, providedDependencies = null) => {
+              continuationStorage.returnedWith = 'continuation';
+              continuationStorage.providedDependencies = providedDependencies;
+              continuationStorage.providedValue = providedValue;
+              return continuationSymbol;
+            }
+          : (providedDependencies = null) => {
+              continuationStorage.returnedWith = 'continuation';
+              continuationStorage.providedDependencies = providedDependencies;
+              return continuationSymbol;
+            });
+
+      continuation.exit = (providedValue) => {
+        continuationStorage.returnedWith = 'exit';
+        continuationStorage.providedValue = providedValue;
+        return continuationSymbol;
+      };
+
+      if (baseComposes) {
+        const makeRaiseLike = returnWith =>
+          (callingTransformForThisStep
+            ? (providedValue, providedDependencies = null) => {
+                continuationStorage.returnedWith = returnWith;
+                continuationStorage.providedDependencies = providedDependencies;
+                continuationStorage.providedValue = providedValue;
+                return continuationSymbol;
+              }
+            : (providedDependencies = null) => {
+                continuationStorage.returnedWith = returnWith;
+                continuationStorage.providedDependencies = providedDependencies;
+                return continuationSymbol;
+              });
+
+        continuation.raise = makeRaiseLike('raise');
+        continuation.raiseAbove = makeRaiseLike('raiseAbove');
+      }
+
+      return {continuation, continuationStorage};
+    }
+
+    function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) {
+      const expectingTransform = initialValue !== noTransformSymbol;
+
+      let valueSoFar =
+        (expectingTransform
+          ? initialValue
+          : undefined);
+
+      const availableDependencies = {...initialDependencies};
+
+      if (expectingTransform) {
+        debug(() => [color.bright(`begin composition - transforming from:`), initialValue]);
+      } else {
+        debug(() => color.bright(`begin composition - not transforming`));
+      }
+
+      for (let i = 0; i < steps.length; i++) {
+        const step = steps[i];
+        const isBase = i === steps.length - 1;
+
+        debug(() => [
+          `step #${i+1}` +
+          (isBase
+            ? ` (base):`
+            : ` of ${steps.length}:`),
+          step]);
+
+        const expose =
+          (step.flags
+            ? step.expose
+            : step);
+
+        const callingTransformForThisStep =
+          expectingTransform && expose.transform;
+
+        const filteredDependencies = _filterDependencies(availableDependencies, expose);
+        const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep);
+
+        debug(() => [
+          `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`,
+          `with dependencies:`, filteredDependencies]);
+
+        const result =
+          (callingTransformForThisStep
+            ? expose.transform(valueSoFar, filteredDependencies, continuation)
+            : expose.compute(filteredDependencies, continuation));
+
+        if (result !== continuationSymbol) {
+          debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]);
+
+          if (baseComposes) {
+            throw new TypeError(`Inferred early-exit is disallowed in nested compositions`);
+          }
+
+          debug(() => color.bright(`end composition - exit (inferred)`));
+
+          return result;
+        }
+
+        const {returnedWith} = continuationStorage;
+
+        if (returnedWith === 'exit') {
+          const {providedValue} = continuationStorage;
+
+          debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]);
+          debug(() => color.bright(`end composition - exit (explicit)`));
+
+          if (baseComposes) {
+            return continuationIfApplicable.exit(providedValue);
+          } else {
+            return providedValue;
+          }
+        }
+
+        const {providedValue, providedDependencies} = continuationStorage;
+
+        const continuingWithValue =
+          (expectingTransform
+            ? (callingTransformForThisStep
+                ? providedValue ?? null
+                : valueSoFar ?? null)
+            : undefined);
+
+        const continuingWithDependencies =
+          (providedDependencies
+            ? _assignDependencies(providedDependencies, expose)
+            : null);
+
+        const continuationArgs = [];
+        if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue);
+        if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies);
+
+        debug(() => {
+          const base = `step #${i+1} - result: ` + returnedWith;
+          const parts = [];
+
+          if (callingTransformForThisStep) {
+            if (continuingWithValue === undefined) {
+              parts.push(`(no value)`);
+            } else {
+              parts.push(`value:`, providedValue);
+            }
+          }
+
+          if (continuingWithDependencies !== null) {
+            parts.push(`deps:`, continuingWithDependencies);
+          } else {
+            parts.push(`(no deps)`);
+          }
+
+          if (empty(parts)) {
+            return base;
+          } else {
+            return [base + ' ->', ...parts];
+          }
+        });
+
+        switch (returnedWith) {
+          case 'raise':
+            debug(() =>
+              (isBase
+                ? color.bright(`end composition - raise (base: explicit)`)
+                : color.bright(`end composition - raise`)));
+            return continuationIfApplicable(...continuationArgs);
+
+          case 'raiseAbove':
+            debug(() => color.bright(`end composition - raiseAbove`));
+            return continuationIfApplicable.raise(...continuationArgs);
+
+          case 'continuation':
+            if (isBase) {
+              debug(() => color.bright(`end composition - raise (inferred)`));
+              return continuationIfApplicable(...continuationArgs);
+            } else {
+              Object.assign(availableDependencies, continuingWithDependencies);
+              break;
+            }
+        }
+      }
+    }
+
+    const transformFn =
+      (value, initialDependencies, continuationIfApplicable) =>
+        _computeOrTransform(value, initialDependencies, continuationIfApplicable);
+
+    const computeFn =
+      (initialDependencies, continuationIfApplicable) =>
+        _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable);
+
+    if (baseComposes) {
+      if (anyStepsTransform) expose.transform = transformFn;
+      if (anyStepsCompute) expose.compute = computeFn;
+    } else if (baseUpdates) {
+      expose.transform = transformFn;
+    } else {
+      expose.compute = computeFn;
+    }
+  }
+
+  return constructedDescriptor;
+}
+
+// Evaluates a function with composite debugging enabled, turns debugging
+// off again, and returns the result of the function. This is mostly syntax
+// sugar, but also helps avoid unit tests avoid accidentally printing debug
+// info for a bunch of unrelated composites (due to property enumeration
+// when displaying an unexpected result). Use as so:
+//
+//   Without debugging:
+//     t.same(thing.someProp, value)
+//
+//   With debugging:
+//     t.same(Thing.composite.debug(() => thing.someProp), value)
+//
+export function debug(fn) {
+  compositeFrom.debug = true;
+  const value = fn();
+  compositeFrom.debug = false;
+  return value;
+}
+
+// -- Compositional steps for compositions to nest --
+
+// Provides dependencies exactly as they are (or null if not defined) to the
+// continuation. Although this can *technically* be used to alias existing
+// dependencies to some other name within the middle of a composition, it's
+// intended to be used only as a composition's base - doing so makes the
+// composition as a whole suitable as a step in some other composition,
+// providing the listed (internal) dependencies to later steps just like
+// other compositional steps.
+export {_export as export};
+function _export(mapping) {
+  const mappingEntries = Object.entries(mapping);
+
+  return {
+    annotation: `Thing.composite.export`,
+    flags: {expose: true, compose: true},
+
+    expose: {
+      options: {mappingEntries},
+      dependencies: Object.values(mapping),
+
+      compute({'#options': {mappingEntries}, ...dependencies}, continuation) {
+        const exports = {};
+
+        // Note: This is slightly different behavior from filterProperties,
+        // as defined in sugar.js, which doesn't fall back to null for
+        // properties which don't exist on the original object.
+        for (const [exportKey, dependencyKey] of mappingEntries) {
+          exports[exportKey] =
+            (Object.hasOwn(dependencies, dependencyKey)
+              ? dependencies[dependencyKey]
+              : null);
+        }
+
+        return continuation.raise(exports);
+      }
+    },
+  };
+}
+
+// -- Compositional steps for top-level property descriptors --
+
+// Exposes a dependency exactly as it is; this is typically the base of a
+// composition which was created to serve as one property's descriptor.
+// Since this serves as a base, specify a value for {update} to indicate
+// that the property as a whole updates (and some previous compositional
+// step works with that update value). Set {update: true} to only enable
+// the update flag, or set update to an object to specify a descriptor
+// (e.g. for custom value validation).
+//
+// Please note that this *doesn't* verify that the dependency exists, so
+// if you provide the wrong name or it hasn't been set by a previous
+// compositional step, the property will be exposed as undefined instead
+// of null.
+//
+export function exposeDependency(dependency, {
+  update = false,
+} = {}) {
+  return {
+    annotation: `Thing.composite.exposeDependency`,
+    flags: {expose: true, update: !!update},
+
+    expose: {
+      mapDependencies: {dependency},
+      compute: ({dependency}) => dependency,
+    },
+
+    update:
+      (typeof update === 'object'
+        ? update
+        : null),
+  };
+}
+
+// Exposes a constant value exactly as it is; like exposeDependency, this
+// is typically the base of a composition serving as a particular property
+// descriptor. It generally follows steps which will conditionally early
+// exit with some other value, with the exposeConstant base serving as the
+// fallback default value. Like exposeDependency, set {update} to true or
+// an object to indicate that the property as a whole updates.
+export function exposeConstant(value, {
+  update = false,
+} = {}) {
+  return {
+    annotation: `Thing.composite.exposeConstant`,
+    flags: {expose: true, update: !!update},
+
+    expose: {
+      options: {value},
+      compute: ({'#options': {value}}) => value,
+    },
+
+    update:
+      (typeof update === 'object'
+        ? update
+        : null),
+  };
+}
+
+// Checks the availability of a dependency or the update value and provides
+// the result to later steps under '#availability' (by default). This is
+// mainly intended for use by the more specific utilities, which you should
+// consider using instead. Customize {mode} to select one of these modes,
+// or leave unset and default to 'null':
+//
+// * 'null':  Check that the value isn't null.
+// * 'empty': Check that the value is neither null nor an empty array.
+// * 'falsy': Check that the value isn't false when treated as a boolean
+//            (nor an empty array). Keep in mind this will also be false
+//            for values like zero and the empty string!
+//
+export function withResultOfAvailabilityCheck({
+  fromUpdateValue,
+  fromDependency,
+  mode = 'null',
+  to = '#availability',
+}) {
+  if (!['null', 'empty', 'falsy'].includes(mode)) {
+    throw new TypeError(`Expected mode to be null, empty, or falsy`);
+  }
+
+  if (fromUpdateValue && fromDependency) {
+    throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`);
+  }
+
+  if (!fromUpdateValue && !fromDependency) {
+    throw new TypeError(`Missing dependency name (or fromUpdateValue)`);
+  }
+
+  const checkAvailability = (value, mode) => {
+    switch (mode) {
+      case 'null': return value !== null;
+      case 'empty': return !empty(value);
+      case 'falsy': return !!value && (!Array.isArray(value) || !empty(value));
+      default: return false;
+    }
+  };
+
+  if (fromDependency) {
+    return {
+      annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`,
+      flags: {expose: true, compose: true},
+      expose: {
+        mapDependencies: {from: fromDependency},
+        mapContinuation: {to},
+        options: {mode},
+        compute: ({from, '#options': {mode}}, continuation) =>
+          continuation({to: checkAvailability(from, mode)}),
+      },
+    };
+  } else {
+    return {
+      annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`,
+      flags: {expose: true, compose: true},
+      expose: {
+        mapContinuation: {to},
+        options: {mode},
+        transform: (value, {'#options': {mode}}, continuation) =>
+          continuation(value, {to: checkAvailability(value, mode)}),
+      },
+    };
+  }
+}
+
+// Exposes a dependency as it is, or continues if it's unavailable.
+// See withResultOfAvailabilityCheck for {mode} options!
+export function exposeDependencyOrContinue(dependency, {
+  mode = 'null',
+} = {}) {
+  return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [
+    withResultOfAvailabilityCheck({
+      fromDependency: dependency,
+      mode,
+    }),
+
+    {
+      dependencies: ['#availability'],
+      compute: ({'#availability': availability}, continuation) =>
+        (availability
+          ? continuation()
+          : continuation.raise()),
+    },
+
+    {
+      mapDependencies: {dependency},
+      compute: ({dependency}, continuation) =>
+        continuation.exit(dependency),
+    },
+  ]);
+}
+
+// Exposes the update value of an {update: true} property as it is,
+// or continues if it's unavailable. See withResultOfAvailabilityCheck
+// for {mode} options!
+export function exposeUpdateValueOrContinue({
+  mode = 'null',
+} = {}) {
+  return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [
+    withResultOfAvailabilityCheck({
+      fromUpdateValue: true,
+      mode,
+    }),
+
+    {
+      dependencies: ['#availability'],
+      compute: ({'#availability': availability}, continuation) =>
+        (availability
+          ? continuation()
+          : continuation.raise()),
+    },
+
+    {
+      transform: (value, {}, continuation) =>
+        continuation.exit(value),
+    },
+  ]);
+}
+
+// Early exits if an availability check has failed.
+// This is for internal use only - use `earlyExitWithoutDependency` or
+// `earlyExitWIthoutUpdateValue` instead.
+export function earlyExitIfAvailabilityCheckFailed({
+  availability = '#availability',
+  value = null,
+} = {}) {
+  return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [
+    {
+      mapDependencies: {availability},
+      compute: ({availability}, continuation) =>
+        (availability
+          ? continuation.raise()
+          : continuation()),
+    },
+
+    {
+      options: {value},
+      compute: ({'#options': {value}}, continuation) =>
+        continuation.exit(value),
+    },
+  ]);
+}
+
+// Early exits if a dependency isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export function earlyExitWithoutDependency(dependency, {
+  mode = 'null',
+  value = null,
+} = {}) {
+  return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [
+    withResultOfAvailabilityCheck({fromDependency: dependency, mode}),
+    earlyExitIfAvailabilityCheckFailed({value}),
+  ]);
+}
+
+// Early exits if this property's update value isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export function earlyExitWithoutUpdateValue({
+  mode = 'null',
+  value = null,
+} = {}) {
+  return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [
+    withResultOfAvailabilityCheck({fromUpdateValue: true, mode}),
+    earlyExitIfAvailabilityCheckFailed({value}),
+  ]);
+}
+
+// Raises if a dependency isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export function raiseWithoutDependency(dependency, {
+  mode = 'null',
+  map = {},
+  raise = {},
+} = {}) {
+  return compositeFrom(`Thing.composite.raiseWithoutDependency`, [
+    withResultOfAvailabilityCheck({fromDependency: dependency, mode}),
+
+    {
+      dependencies: ['#availability'],
+      compute: ({'#availability': availability}, continuation) =>
+        (availability
+          ? continuation.raise()
+          : continuation()),
+    },
+
+    {
+      options: {raise},
+      mapContinuation: map,
+      compute: ({'#options': {raise}}, continuation) =>
+        continuation.raiseAbove(raise),
+    },
+  ]);
+}
+
+// Raises if this property's update value isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export function raiseWithoutUpdateValue({
+  mode = 'null',
+  map = {},
+  raise = {},
+} = {}) {
+  return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [
+    withResultOfAvailabilityCheck({fromUpdateValue: true, mode}),
+
+    {
+      mapDependencies: {availability},
+      compute: ({availability}, continuation) =>
+        (availability
+          ? continuation.raise()
+          : continuation()),
+    },
+
+    {
+      options: {raise},
+      mapContinuation: map,
+      compute: ({'#options': {raise}}, continuation) =>
+        continuation.raiseAbove(raise),
+    },
+  ]);
+}
+
+// -- Compositional steps for processing data --
+
+// Resolves the contribsByRef contained in the provided dependency,
+// providing (named by the second argument) the result. "Resolving"
+// means mapping the "who" reference of each contribution to an artist
+// object, and filtering out those whose "who" doesn't match any artist.
+export function withResolvedContribs({from, to}) {
+  return {
+    annotation: `Thing.composite.withResolvedContribs`,
+    flags: {expose: true, compose: true},
+
+    expose: {
+      dependencies: ['artistData'],
+      mapDependencies: {from},
+      mapContinuation: {to},
+      compute: ({artistData, from}, continuation) =>
+        continuation({
+          to: Thing.findArtistsFromContribs(from, artistData),
+        }),
+    },
+  };
+}
+
+// Resolves a reference by using the provided find function to match it
+// within the provided thingData dependency. This will early exit if the
+// data dependency is null, or, if earlyExitIfNotFound is set to true,
+// if the find function doesn't match anything for the reference.
+// Otherwise, the data object is provided on the output dependency;
+// or null, if the reference doesn't match anything or itself was null
+// to begin with.
+export function withResolvedReference({
+  ref,
+  data,
+  to,
+  find: findFunction,
+  earlyExitIfNotFound = false,
+}) {
+  return compositeFrom(`Thing.composite.withResolvedReference`, [
+    raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}),
+    earlyExitWithoutDependency(data),
+
+    {
+      options: {findFunction, earlyExitIfNotFound},
+      mapDependencies: {ref, data},
+      mapContinuation: {match: to},
+
+      compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) {
+        const match = findFunction(ref, data, {mode: 'quiet'});
+
+        if (match === null && earlyExitIfNotFound) {
+          return continuation.exit(null);
+        }
+
+        return continuation.raise({match});
+      },
+    },
+  ]);
+}
+
+// Check out the info on Thing.common.reverseReferenceList!
+// This is its composable form.
+export function withReverseReferenceList({
+  data,
+  to = '#reverseReferenceList',
+  refList: refListProperty,
+}) {
+  return compositeFrom(`Thing.common.reverseReferenceList`, [
+    earlyExitWithoutDependency(data, {value: []}),
+
+    {
+      dependencies: ['this'],
+      mapDependencies: {data},
+      mapContinuation: {to},
+      options: {refListProperty},
+
+      compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) =>
+        continuation({
+          to: data.filter(thing => thing[refListProperty].includes(thisThing)),
+        }),
+    },
+  ]);
+}