« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/composite/things/track-section/index.js1
-rw-r--r--src/data/composite/things/track-section/withAlbum.js20
-rw-r--r--src/data/composite/things/track-section/withStartCountingFrom.js8
-rw-r--r--src/data/composite/things/track/index.js1
-rw-r--r--src/data/composite/things/track/withContainingTrackSection.js20
-rw-r--r--src/data/composite/things/track/withDirectorySuffix.js5
-rw-r--r--src/data/composite/things/track/withSuffixDirectoryFromAlbum.js6
-rw-r--r--src/data/composite/things/track/withTrackNumber.js8
-rw-r--r--src/data/thing.js4
-rw-r--r--src/data/things/adventure.js62
-rw-r--r--src/data/things/album.js179
-rw-r--r--src/data/things/art-tag.js3
-rw-r--r--src/data/things/artist.js19
-rw-r--r--src/data/things/artwork.js6
-rw-r--r--src/data/things/content.js8
-rw-r--r--src/data/things/flash.js36
-rw-r--r--src/data/things/group.js12
-rw-r--r--src/data/things/homepage-layout.js6
-rw-r--r--src/data/things/news-entry.js3
-rw-r--r--src/data/things/sorting-rule.js3
-rw-r--r--src/data/things/static-page.js3
-rw-r--r--src/data/things/track.js29
-rw-r--r--src/data/things/wiki-info.js10
-rw-r--r--src/data/yaml.js286
24 files changed, 328 insertions, 410 deletions
diff --git a/src/data/composite/things/track-section/index.js b/src/data/composite/things/track-section/index.js
index f11a2ab5..1da49ea4 100644
--- a/src/data/composite/things/track-section/index.js
+++ b/src/data/composite/things/track-section/index.js
@@ -1,3 +1,2 @@
-export {default as withAlbum} from './withAlbum.js';
 export {default as withContinueCountingFrom} from './withContinueCountingFrom.js';
 export {default as withStartCountingFrom} from './withStartCountingFrom.js';
diff --git a/src/data/composite/things/track-section/withAlbum.js b/src/data/composite/things/track-section/withAlbum.js
deleted file mode 100644
index e257062e..00000000
--- a/src/data/composite/things/track-section/withAlbum.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Gets the track section's album.
-
-import {templateCompositeFrom} from '#composite';
-
-import {withUniqueReferencingThing} from '#composite/wiki-data';
-import {soupyReverse} from '#composite/wiki-properties';
-
-export default templateCompositeFrom({
-  annotation: `withAlbum`,
-
-  outputs: ['#album'],
-
-  steps: () => [
-    withUniqueReferencingThing({
-      reverse: soupyReverse.input('albumsWhoseTrackSectionsInclude'),
-    }).outputs({
-      ['#uniqueReferencingThing']: '#album',
-    }),
-  ],
-});
diff --git a/src/data/composite/things/track-section/withStartCountingFrom.js b/src/data/composite/things/track-section/withStartCountingFrom.js
index ef345327..20e18edb 100644
--- a/src/data/composite/things/track-section/withStartCountingFrom.js
+++ b/src/data/composite/things/track-section/withStartCountingFrom.js
@@ -3,8 +3,6 @@ import {input, templateCompositeFrom} from '#composite';
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 import {withNearbyItemFromList, withPropertyFromObject} from '#composite/data';
 
-import withAlbum from './withAlbum.js';
-
 export default templateCompositeFrom({
   annotation: `withStartCountingFrom`,
 
@@ -29,15 +27,13 @@ export default templateCompositeFrom({
           : continuation.raiseOutput({'#startCountingFrom': from})),
     },
 
-    withAlbum(),
-
     raiseOutputWithoutDependency({
-      dependency: '#album',
+      dependency: 'album',
       output: input.value({'#startCountingFrom': 1}),
     }),
 
     withPropertyFromObject({
-      object: '#album',
+      object: 'album',
       property: input.value('trackSections'),
     }),
 
diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js
index 1c203cd9..be276d25 100644
--- a/src/data/composite/things/track/index.js
+++ b/src/data/composite/things/track/index.js
@@ -3,7 +3,6 @@ export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.
 export {default as inheritContributionListFromMainRelease} from './inheritContributionListFromMainRelease.js';
 export {default as inheritFromMainRelease} from './inheritFromMainRelease.js';
 export {default as withAllReleases} from './withAllReleases.js';
-export {default as withContainingTrackSection} from './withContainingTrackSection.js';
 export {default as withCoverArtistContribs} from './withCoverArtistContribs.js';
 export {default as withDate} from './withDate.js';
 export {default as withDirectorySuffix} from './withDirectorySuffix.js';
diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js
deleted file mode 100644
index 3d4d081e..00000000
--- a/src/data/composite/things/track/withContainingTrackSection.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Gets the track section containing this track from its album's track list.
-
-import {templateCompositeFrom} from '#composite';
-
-import {withUniqueReferencingThing} from '#composite/wiki-data';
-import {soupyReverse} from '#composite/wiki-properties';
-
-export default templateCompositeFrom({
-  annotation: `withContainingTrackSection`,
-
-  outputs: ['#trackSection'],
-
-  steps: () => [
-    withUniqueReferencingThing({
-      reverse: soupyReverse.input('trackSectionsWhichInclude'),
-    }).outputs({
-      ['#uniqueReferencingThing']: '#trackSection',
-    }),
-  ],
-});
diff --git a/src/data/composite/things/track/withDirectorySuffix.js b/src/data/composite/things/track/withDirectorySuffix.js
index c3651491..13813eeb 100644
--- a/src/data/composite/things/track/withDirectorySuffix.js
+++ b/src/data/composite/things/track/withDirectorySuffix.js
@@ -3,7 +3,6 @@ import {input, templateCompositeFrom} from '#composite';
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 
-import withContainingTrackSection from './withContainingTrackSection.js';
 import withSuffixDirectoryFromAlbum from './withSuffixDirectoryFromAlbum.js';
 
 export default templateCompositeFrom({
@@ -20,10 +19,8 @@ export default templateCompositeFrom({
       output: input.value({'#directorySuffix': null}),
     }),
 
-    withContainingTrackSection(),
-
     withPropertyFromObject({
-      object: '#trackSection',
+      object: 'trackSection',
       property: input.value('directorySuffix'),
     }).outputs({
       '#trackSection.directorySuffix': '#directorySuffix',
diff --git a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
index 30c777b6..047077fd 100644
--- a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
+++ b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
@@ -3,8 +3,6 @@ import {input, templateCompositeFrom} from '#composite';
 import {withResultOfAvailabilityCheck} from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 
-import withContainingTrackSection from './withContainingTrackSection.js';
-
 export default templateCompositeFrom({
   annotation: `withSuffixDirectoryFromAlbum`,
 
@@ -37,10 +35,8 @@ export default templateCompositeFrom({
           : continuation()),
     },
 
-    withContainingTrackSection(),
-
     withPropertyFromObject({
-      object: '#trackSection',
+      object: 'trackSection',
       property: input.value('suffixTrackDirectories'),
     }).outputs({
       '#trackSection.suffixTrackDirectories': '#suffixDirectoryFromAlbum',
diff --git a/src/data/composite/things/track/withTrackNumber.js b/src/data/composite/things/track/withTrackNumber.js
index 61428e8c..bb0f1366 100644
--- a/src/data/composite/things/track/withTrackNumber.js
+++ b/src/data/composite/things/track/withTrackNumber.js
@@ -3,25 +3,21 @@ import {input, templateCompositeFrom} from '#composite';
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 import {withIndexInList, withPropertiesFromObject} from '#composite/data';
 
-import withContainingTrackSection from './withContainingTrackSection.js';
-
 export default templateCompositeFrom({
   annotation: `withTrackNumber`,
 
   outputs: ['#trackNumber'],
 
   steps: () => [
-    withContainingTrackSection(),
-
     // Zero is the fallback, not one, but in most albums the first track
     // (and its intended output by this composition) will be one.
     raiseOutputWithoutDependency({
-      dependency: '#trackSection',
+      dependency: 'trackSection',
       output: input.value({'#trackNumber': 0}),
     }),
 
     withPropertiesFromObject({
-      object: '#trackSection',
+      object: 'trackSection',
       properties: input.value(['tracks', 'startCountingFrom']),
     }),
 
diff --git a/src/data/thing.js b/src/data/thing.js
index 4fbad5f5..32eff4d1 100644
--- a/src/data/thing.js
+++ b/src/data/thing.js
@@ -10,6 +10,10 @@ export default class Thing extends CacheableObject {
   static referenceType = Symbol.for('Thing.referenceType');
   static friendlyName = Symbol.for('Thing.friendlyName');
 
+  static wikiData = Symbol.for('Thing.wikiData');
+  static oneInstancePerWiki = Symbol.for('Thing.oneThingPerWiki');
+  static constitutibleProperties = Symbol.for('Thing.constitutibleProperties');
+
   static getPropertyDescriptors = Symbol.for('Thing.getPropertyDescriptors');
   static getSerializeDescriptors = Symbol.for('Thing.getSerializeDescriptors');
 
diff --git a/src/data/things/adventure.js b/src/data/things/adventure.js
index ed5da39b..98b23f39 100644
--- a/src/data/things/adventure.js
+++ b/src/data/things/adventure.js
@@ -16,6 +16,7 @@ import {Flash, FlashAct} from './flash.js';
 
 export class Adventure extends Thing {
   static [Thing.referenceType] = 'adventure';
+  static [Thing.wikiData] = 'adventureData';
 
   static [Thing.getPropertyDescriptors] = ({FlashAct}) => ({
     // > Internal relationships
@@ -63,56 +64,41 @@ export class Adventure extends Thing {
         ? AdventureFlashAct
         : AdventureFlash),
 
-    save(results) {
-      const adventureData = [];
-      const flashActData = [];
-      const flashData = [];
+    connect({header: adventure, entries}) {
+      const acts = [];
 
-      for (const {header: adventure, entries} of results) {
-        const acts = [];
+      let thing, i;
+      for (i = 0; thing = entries[i]; i++) {
+        if (thing.isFlashAct) {
+          const act = thing;
+          const flashes = [];
 
-        let thing, i;
-        for (i = 0; thing = entries[i]; i++) {
-          if (thing.isFlashAct) {
-            const act = thing;
-            const flashes = [];
+          for (i++; thing = entries[i]; i++) {
+            if (thing.isFlash) {
+              const flash = thing;
 
-            for (i++; thing = entries[i]; i++) {
-              if (thing.isFlash) {
-                const flash = thing;
+              flash.act = act;
+              flashes.push(flash);
 
-                flash.act = act;
-                flashes.push(flash);
-                flashData.push(flash);
-
-                continue;
-              }
-
-              i--;
-              break;
+              continue;
             }
 
-            act.flashes = flashes;
-            acts.push(act);
-            flashActData.push(act);
-
-            continue;
+            i--;
+            break;
           }
 
-          if (thing.isFlash) {
-            throw new Error(`Flashes must be under a flash act`);
-          }
+          act.flashes = flashes;
+          acts.push(act);
+
+          continue;
         }
 
-        adventure.acts = acts;
-        adventureData.push(adventure);
+        if (thing.isFlash) {
+          throw new Error(`Flashes must be under a flash act`);
+        }
       }
 
-      return {
-        adventureData,
-        flashActData,
-        flashData,
-      };
+      adventure.acts = acts;
     },
   });
 }
diff --git a/src/data/things/album.js b/src/data/things/album.js
index c0042d25..defb8a87 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -78,11 +78,18 @@ import {
 } from '#composite/wiki-properties';
 
 import {withCoverArtDate, withTracks} from '#composite/things/album';
-import {withAlbum, withContinueCountingFrom, withStartCountingFrom}
+import {withContinueCountingFrom, withStartCountingFrom}
   from '#composite/things/track-section';
 
 export class Album extends Thing {
   static [Thing.referenceType] = 'album';
+  static [Thing.wikiData] = 'albumData';
+
+  static [Thing.constitutibleProperties] = [
+    'coverArtworks',
+    'wallpaperArtwork',
+    'bannerArtwork',
+  ];
 
   static [Thing.getPropertyDescriptors] = ({
     AdditionalFile,
@@ -569,20 +576,6 @@ export class Album extends Thing {
   };
 
   static [Thing.reverseSpecs] = {
-    albumsWhoseTracksInclude: {
-      bindTo: 'albumData',
-
-      referencing: album => [album],
-      referenced: album => album.tracks,
-    },
-
-    albumsWhoseTrackSectionsInclude: {
-      bindTo: 'albumData',
-
-      referencing: album => [album],
-      referenced: album => album.trackSections,
-    },
-
     albumsWhoseArtworksFeature: {
       bindTo: 'albumData',
 
@@ -882,101 +875,48 @@ export class Album extends Thing {
         ? TrackSection
         : Track),
 
-    save(results) {
-      const albumData = [];
-      const trackSectionData = [];
-      const trackData = [];
-
-      const artworkData = [];
-      const commentaryData = [];
-      const creditingSourceData = [];
-      const referencingSourceData = [];
-      const lyricsData = [];
-
-      for (const {header: album, entries} of results) {
-        const trackSections = [];
-
-        let currentTrackSection = new TrackSection();
-        let currentTrackSectionTracks = [];
-
-        Object.assign(currentTrackSection, {
-          name: `Default Track Section`,
-          isDefaultTrackSection: true,
-        });
-
-        const closeCurrentTrackSection = () => {
-          if (
-            currentTrackSection.isDefaultTrackSection &&
-            empty(currentTrackSectionTracks)
-          ) {
-            return;
-          }
-
-          currentTrackSection.tracks =
-            currentTrackSectionTracks;
-
-          trackSections.push(currentTrackSection);
-          trackSectionData.push(currentTrackSection);
-        };
-
-        for (const entry of entries) {
-          if (entry instanceof TrackSection) {
-            closeCurrentTrackSection();
-            currentTrackSection = entry;
-            currentTrackSectionTracks = [];
-            continue;
-          }
-
-          currentTrackSectionTracks.push(entry);
-          trackData.push(entry);
-
-          // Set the track's album before accessing its list of artworks.
-          // The existence of its artwork objects may depend on access to
-          // its album's 'Default Track Cover Artists'.
-          entry.album = album;
-
-          artworkData.push(...entry.trackArtworks);
-          commentaryData.push(...entry.commentary);
-          creditingSourceData.push(...entry.creditingSources);
-          referencingSourceData.push(...entry.referencingSources);
-
-          // TODO: As exposed, Track.lyrics tries to inherit from the main
-          // release, which is impossible before the data's been linked.
-          // We just use the update value here. But it's icky!
-          lyricsData.push(...CacheableObject.getUpdateValue(entry, 'lyrics') ?? []);
-        }
-
-        closeCurrentTrackSection();
+    connect({header: album, entries}) {
+      const trackSections = [];
 
-        albumData.push(album);
+      let currentTrackSection = new TrackSection();
+      let currentTrackSectionTracks = [];
 
-        artworkData.push(...album.coverArtworks);
+      Object.assign(currentTrackSection, {
+        name: `Default Track Section`,
+        isDefaultTrackSection: true,
+      });
 
-        if (album.bannerArtwork) {
-          artworkData.push(album.bannerArtwork);
+      const closeCurrentTrackSection = () => {
+        if (
+          currentTrackSection.isDefaultTrackSection &&
+          empty(currentTrackSectionTracks)
+        ) {
+          return;
         }
 
-        if (album.wallpaperArtwork) {
-          artworkData.push(album.wallpaperArtwork);
+        currentTrackSection.tracks = currentTrackSectionTracks;
+        currentTrackSection.album = album;
+
+        trackSections.push(currentTrackSection);
+      };
+
+      for (const entry of entries) {
+        if (entry instanceof TrackSection) {
+          closeCurrentTrackSection();
+          currentTrackSection = entry;
+          currentTrackSectionTracks = [];
+          continue;
         }
 
-        commentaryData.push(...album.commentary);
-        creditingSourceData.push(...album.creditingSources);
+        entry.album = album;
+        entry.trackSection = currentTrackSection;
 
-        album.trackSections = trackSections;
+        currentTrackSectionTracks.push(entry);
       }
 
-      return {
-        albumData,
-        trackSectionData,
-        trackData,
+      closeCurrentTrackSection();
 
-        artworkData,
-        commentaryData,
-        creditingSourceData,
-        referencingSourceData,
-        lyricsData,
-      };
+      album.trackSections = trackSections;
     },
 
     sort({albumData, trackData}) {
@@ -1041,10 +981,15 @@ export class Album extends Thing {
 export class TrackSection extends Thing {
   static [Thing.friendlyName] = `Track Section`;
   static [Thing.referenceType] = `track-section`;
+  static [Thing.wikiData] = 'trackSectionData';
 
   static [Thing.getPropertyDescriptors] = ({Track}) => ({
     // Update & expose
 
+    album: thing({
+      class: input.value(Album),
+    }),
+
     name: name('Unnamed Track Section'),
 
     unqualifiedDirectory: directory(),
@@ -1054,10 +999,8 @@ export class TrackSection extends Thing {
         validate: input.value(isDirectory),
       }),
 
-      withAlbum(),
-
       withPropertyFromObject({
-        object: '#album',
+        object: 'album',
         property: input.value('directorySuffix'),
       }),
 
@@ -1069,10 +1012,8 @@ export class TrackSection extends Thing {
         validate: input.value(isBoolean),
       }),
 
-      withAlbum(),
-
       withPropertyFromObject({
-        object: '#album',
+        object: 'album',
         property: input.value('suffixTrackDirectories'),
       }),
 
@@ -1084,10 +1025,8 @@ export class TrackSection extends Thing {
         validate: input.value(isColor),
       }),
 
-      withAlbum(),
-
       withPropertyFromObject({
-        object: '#album',
+        object: 'album',
         property: input.value('color'),
       }),
 
@@ -1109,10 +1048,8 @@ export class TrackSection extends Thing {
         validate: input.value(isBoolean),
       }),
 
-      withAlbum(),
-
       withPropertyFromObject({
-        object: '#album',
+        object: 'album',
         property: input.value('countTracksInArtistTotals'),
       }),
 
@@ -1123,11 +1060,6 @@ export class TrackSection extends Thing {
 
     description: contentString(),
 
-    album: [
-      withAlbum(),
-      exposeDependency({dependency: '#album'}),
-    ],
-
     tracks: thingList({
       class: input.value(Track),
     }),
@@ -1145,14 +1077,12 @@ export class TrackSection extends Thing {
     ],
 
     directory: [
-      withAlbum(),
-
       exitWithoutDependency({
-        dependency: '#album',
+        dependency: 'album',
       }),
 
       withPropertyFromObject({
-        object: '#album',
+        object: 'album',
         property: input.value('directory'),
       }),
 
@@ -1193,15 +1123,6 @@ export class TrackSection extends Thing {
     },
   };
 
-  static [Thing.reverseSpecs] = {
-    trackSectionsWhichInclude: {
-      bindTo: 'trackSectionData',
-
-      referencing: trackSection => [trackSection],
-      referenced: trackSection => trackSection.tracks,
-    },
-  };
-
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Section': {property: 'name'},
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index fff724cb..3570b2e7 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -40,6 +40,7 @@ import {withAllDescendantArtTags, withAncestorArtTagBaobabTree}
 export class ArtTag extends Thing {
   static [Thing.referenceType] = 'tag';
   static [Thing.friendlyName] = `Art Tag`;
+  static [Thing.wikiData] = 'artTagData';
 
   static [Thing.getPropertyDescriptors] = ({AdditionalName}) => ({
     // Update & expose
@@ -210,8 +211,6 @@ export class ArtTag extends Thing {
     documentMode: allTogether,
     documentThing: ArtTag,
 
-    save: (results) => ({artTagData: results}),
-
     sort({artTagData}) {
       sortAlphabetically(artTagData);
     },
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 24c99698..a5601d60 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -41,7 +41,11 @@ import {artistTotalDuration} from '#composite/things/artist';
 
 export class Artist extends Thing {
   static [Thing.referenceType] = 'artist';
-  static [Thing.wikiDataArray] = 'artistData';
+  static [Thing.wikiData] = 'artistData';
+
+  static [Thing.constitutibleProperties] = [
+    'avatarArtwork', // from inline fields
+  ];
 
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
@@ -343,19 +347,6 @@ export class Artist extends Thing {
     documentMode: allInOne,
     documentThing: Artist,
 
-    save(results) {
-      const artists = results;
-      const artistAliases = artists.flatMap(artist => artist.artistAliases);
-      const artistData = [...artists, ...artistAliases];
-
-      const artworkData =
-        artistData
-          .filter(artist => artist.hasAvatar)
-          .map(artist => artist.avatarArtwork);
-
-      return {artistData, artworkData};
-    },
-
     sort({artistData}) {
       sortAlphabetically(artistData);
     },
diff --git a/src/data/things/artwork.js b/src/data/things/artwork.js
index 916aac0a..c1ae4f62 100644
--- a/src/data/things/artwork.js
+++ b/src/data/things/artwork.js
@@ -64,6 +64,12 @@ import {
 
 export class Artwork extends Thing {
   static [Thing.referenceType] = 'artwork';
+  static [Thing.wikiData] = 'artworkData';
+
+  static [Thing.constitutibleProperties] = [
+    // Contributions currently aren't being observed for constitution.
+    // 'artistContribs', // from attached artwork or thing
+  ];
 
   static [Thing.getPropertyDescriptors] = ({ArtTag}) => ({
     // Update & expose
diff --git a/src/data/things/content.js b/src/data/things/content.js
index a3dfc183..95836abd 100644
--- a/src/data/things/content.js
+++ b/src/data/things/content.js
@@ -154,6 +154,8 @@ export class ContentEntry extends Thing {
 }
 
 export class CommentaryEntry extends ContentEntry {
+  static [Thing.wikiData] = 'commentaryData';
+
   static [Thing.getPropertyDescriptors] = () => ({
     // Expose only
 
@@ -170,6 +172,8 @@ export class CommentaryEntry extends ContentEntry {
 }
 
 export class LyricsEntry extends ContentEntry {
+  static [Thing.wikiData] = 'lyricsData';
+
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
 
@@ -223,6 +227,8 @@ export class LyricsEntry extends ContentEntry {
 }
 
 export class CreditingSourcesEntry extends ContentEntry {
+  static [Thing.wikiData] = 'creditingSourceData';
+
   static [Thing.getPropertyDescriptors] = () => ({
     // Expose only
 
@@ -235,6 +241,8 @@ export class CreditingSourcesEntry extends ContentEntry {
 }
 
 export class ReferencingSourcesEntry extends ContentEntry {
+  static [Thing.wikiData] = 'referencingSourceData';
+
   static [Thing.getPropertyDescriptors] = () => ({
     // Expose only
 
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 5c9023fa..19f6093e 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -46,6 +46,11 @@ import {
 
 export class Flash extends Thing {
   static [Thing.referenceType] = 'flash';
+  static [Thing.wikiData] = 'flashData';
+
+  static [Thing.constitutibleProperties] = [
+    'coverArtwork', // from inline fields
+  ];
 
   static [Thing.getPropertyDescriptors] = ({
     AdditionalName,
@@ -274,6 +279,7 @@ export class Flash extends Thing {
 export class FlashAct extends Thing {
   static [Thing.referenceType] = 'flash-act';
   static [Thing.friendlyName] = `Flash Act`;
+  static [Thing.wikiData] = 'flashActData';
 
   static [Thing.getPropertyDescriptors] = ({Flash, FlashSide}) => ({
     // Update & expose
@@ -355,6 +361,7 @@ export class FlashAct extends Thing {
 export class FlashSide extends Thing {
   static [Thing.referenceType] = 'flash-side';
   static [Thing.friendlyName] = `Flash Side`;
+  static [Thing.wikiData] = 'flashSideData';
 
   static [Thing.getPropertyDescriptors] = ({FlashAct}) => ({
     // Update & expose
@@ -421,15 +428,7 @@ export class FlashSide extends Thing {
         ? FlashAct
         : Flash),
 
-    save(results) {
-      const flashSideData = [];
-      const flashActData = [];
-      const flashData = [];
-
-      const artworkData = [];
-      const commentaryData = [];
-      const creditingSourceData = [];
-
+    connect(results) {
       let thing, i;
 
       for (i = 0; thing = results[i]; i++) {
@@ -449,11 +448,6 @@ export class FlashSide extends Thing {
                   flash.act = act;
                   flashes.push(flash);
 
-                  flashData.push(flash);
-                  artworkData.push(flash.coverArtwork);
-                  commentaryData.push(...flash.commentary);
-                  creditingSourceData.push(...flash.creditingSources);
-
                   continue;
                 }
 
@@ -465,8 +459,6 @@ export class FlashSide extends Thing {
               act.flashes = flashes;
               acts.push(act);
 
-              flashActData.push(act);
-
               continue;
             }
 
@@ -480,8 +472,6 @@ export class FlashSide extends Thing {
 
           side.acts = acts;
 
-          flashSideData.push(side);
-
           continue;
         }
 
@@ -493,16 +483,6 @@ export class FlashSide extends Thing {
           throw new Error(`Flashes must be under a side and act`);
         }
       }
-
-      return {
-        flashSideData,
-        flashActData,
-        flashData,
-
-        artworkData,
-        commentaryData,
-        creditingSourceData,
-      };
     },
 
     sort({flashData}) {
diff --git a/src/data/things/group.js b/src/data/things/group.js
index 0935dc93..ac051343 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -34,6 +34,7 @@ import {
 
 export class Group extends Thing {
   static [Thing.referenceType] = 'group';
+  static [Thing.wikiData] = 'groupData';
 
   static [Thing.getPropertyDescriptors] = ({Album, Artist, Series}) => ({
     // Update & expose
@@ -217,7 +218,7 @@ export class Group extends Thing {
         ? GroupCategory
         : Group),
 
-    save(results) {
+    connect(results) {
       let groupCategory;
       let groupRefs = [];
 
@@ -241,12 +242,6 @@ export class Group extends Thing {
       if (groupCategory) {
         Object.assign(groupCategory, {groups: groupRefs});
       }
-
-      const groupData = results.filter(x => x instanceof Group);
-      const groupCategoryData = results.filter(x => x instanceof GroupCategory);
-      const seriesData = groupData.flatMap(group => group.serieses);
-
-      return {groupData, groupCategoryData, seriesData};
     },
 
     // Groups aren't sorted at all, always preserving the order in the data
@@ -258,6 +253,7 @@ export class Group extends Thing {
 export class GroupCategory extends Thing {
   static [Thing.referenceType] = 'group-category';
   static [Thing.friendlyName] = `Group Category`;
+  static [Thing.wikiData] = 'groupCategoryData';
 
   static [Thing.getPropertyDescriptors] = ({Group}) => ({
     // Update & expose
@@ -310,6 +306,8 @@ export class GroupCategory extends Thing {
 }
 
 export class Series extends Thing {
+  static [Thing.wikiData] = 'seriesData';
+
   static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({
     // Update & expose
 
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js
index 7c97935e..5da13e37 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -32,6 +32,8 @@ import {
 
 export class HomepageLayout extends Thing {
   static [Thing.friendlyName] = `Homepage Layout`;
+  static [Thing.wikiData] = 'homepageLayout';
+  static [Thing.oneInstancePerWiki] = true;
 
   static [Thing.getPropertyDescriptors] = ({HomepageLayoutSection}) => ({
     // Update & expose
@@ -102,7 +104,7 @@ export class HomepageLayout extends Thing {
       return null;
     },
 
-    save(results) {
+    connect(results) {
       if (!empty(results) && !(results[0] instanceof HomepageLayout)) {
         throw new Error(`Expected 'Homepage' document at top of homepage layout file`);
       }
@@ -145,8 +147,6 @@ export class HomepageLayout extends Thing {
       closeCurrentSection();
 
       homepageLayout.sections = sections;
-
-      return {homepageLayout};
     },
   });
 }
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index 28289f53..e5467a46 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -12,6 +12,7 @@ import {contentString, directory, name, simpleDate}
 export class NewsEntry extends Thing {
   static [Thing.referenceType] = 'news-entry';
   static [Thing.friendlyName] = `News Entry`;
+  static [Thing.wikiData] = 'newsData';
 
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
@@ -72,8 +73,6 @@ export class NewsEntry extends Thing {
     documentMode: allInOne,
     documentThing: NewsEntry,
 
-    save: (results) => ({newsData: results}),
-
     sort({newsData}) {
       sortChronologically(newsData, {latestFirst: true});
     },
diff --git a/src/data/things/sorting-rule.js b/src/data/things/sorting-rule.js
index 8ed3861a..e113955f 100644
--- a/src/data/things/sorting-rule.js
+++ b/src/data/things/sorting-rule.js
@@ -38,6 +38,7 @@ function isSelectFollowingEntry(value) {
 
 export class SortingRule extends Thing {
   static [Thing.friendlyName] = `Sorting Rule`;
+  static [Thing.wikiData] = 'sortingRules';
 
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
@@ -77,8 +78,6 @@ export class SortingRule extends Thing {
       (document['Sort Documents']
         ? DocumentSortingRule
         : null),
-
-    save: (results) => ({sortingRules: results}),
   });
 
   check(opts) {
diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js
index 28167df2..617bc940 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -15,6 +15,7 @@ import {contentString, directory, flag, name, simpleString}
 export class StaticPage extends Thing {
   static [Thing.referenceType] = 'static';
   static [Thing.friendlyName] = `Static Page`;
+  static [Thing.wikiData] = 'staticPageData';
 
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
@@ -86,8 +87,6 @@ export class StaticPage extends Thing {
     documentMode: onePerFile,
     documentThing: StaticPage,
 
-    save: (results) => ({staticPageData: results}),
-
     sort({staticPageData}) {
       sortAlphabetically(staticPageData);
     },
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 0d565086..4a24a9e0 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -76,7 +76,6 @@ import {
   inheritContributionListFromMainRelease,
   inheritFromMainRelease,
   withAllReleases,
-  withContainingTrackSection,
   withCoverArtistContribs,
   withDate,
   withDirectorySuffix,
@@ -92,6 +91,16 @@ import {
 
 export class Track extends Thing {
   static [Thing.referenceType] = 'track';
+  static [Thing.wikiData] = 'trackData';
+
+  static [Thing.constitutibleProperties] = [
+    // Contributions currently aren't being observed for constitution.
+    // 'artistContribs', // from main release or album
+    // 'contributorContribs', // from main release
+    // 'coverArtistContribs', // from main release
+
+    'trackArtworks', // from inline fields
+  ];
 
   static [Thing.getPropertyDescriptors] = ({
     AdditionalFile,
@@ -103,6 +112,7 @@ export class Track extends Thing {
     CreditingSourcesEntry,
     LyricsEntry,
     ReferencingSourcesEntry,
+    TrackSection,
     WikiInfo,
   }) => ({
     // > Update & expose - Internal relationships
@@ -111,6 +121,10 @@ export class Track extends Thing {
       class: input.value(Album),
     }),
 
+    trackSection: thing({
+      class: input.value(TrackSection),
+    }),
+
     // > Update & expose - Identifying metadata
 
     name: name('Unnamed Track'),
@@ -263,10 +277,8 @@ export class Track extends Thing {
         validate: input.value(isBoolean),
       }),
 
-      withContainingTrackSection(),
-
       withPropertyFromObject({
-        object: '#trackSection',
+        object: 'trackSection',
         property: input.value('countTracksInArtistTotals'),
       }),
 
@@ -285,10 +297,8 @@ export class Track extends Thing {
         validate: input.value(isColor),
       }),
 
-      withContainingTrackSection(),
-
       withPropertyFromObject({
-        object: '#trackSection',
+        object: 'trackSection',
         property: input.value('color'),
       }),
 
@@ -510,6 +520,11 @@ export class Track extends Thing {
 
     commentatorArtists: commentatorArtists(),
 
+    directorySuffix: [
+      withDirectorySuffix(),
+      exposeDependency({dependency: '#directorySuffix'}),
+    ],
+
     date: [
       withDate(),
       exposeDependency({dependency: '#date'}),
diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js
index 7fb6a350..89248d11 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -28,6 +28,8 @@ import {
 
 export class WikiInfo extends Thing {
   static [Thing.friendlyName] = `Wiki Info`;
+  static [Thing.wikiData] = 'wikiInfo';
+  static [Thing.oneInstancePerWiki] = true;
 
   static [Thing.getPropertyDescriptors] = ({Group}) => ({
     // Update & expose
@@ -168,13 +170,5 @@ export class WikiInfo extends Thing {
 
     documentMode: oneDocumentTotal,
     documentThing: WikiInfo,
-
-    save(wikiInfo) {
-      if (!wikiInfo) {
-        return;
-      }
-
-      return {wikiInfo};
-    },
   });
 }
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 13dfd24d..4e6f4502 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -44,6 +44,32 @@ function inspect(value, opts = {}) {
   return nodeInspect(value, {colors: ENABLE_COLOR, ...opts});
 }
 
+function pushWikiData(a, b) {
+  for (const key of Object.keys(b)) {
+    if (Object.hasOwn(a, key)) {
+      if (Array.isArray(a[key])) {
+        if (Array.isArray(b[key])) {
+          a[key].push(...b[key]);
+        } else {
+          throw new Error(`${key} already present, expected array of items to push`);
+        }
+      } else {
+        if (Array.isArray(a[key])) {
+          throw new Error(`${key} already present and not an array, refusing to overwrite`);
+        } else {
+          throw new Error(`${key} already present, refusing to overwrite`);
+        }
+      }
+    } else {
+      if (Array.isArray(b[key])) {
+        a[key] = [...b[key]];
+      } else {
+        a[key] = b[key];
+      }
+    }
+  }
+}
+
 // General function for inputting a single document (usually loaded from YAML)
 // and outputting an instance of a provided Thing subclass.
 //
@@ -161,6 +187,16 @@ function makeProcessDocument(thingConstructor, {
 
     const thing = Reflect.construct(thingConstructor, []);
 
+    const wikiData = {};
+    const flat = [thing];
+    if (thingConstructor[Thing.wikiData]) {
+      if (thingConstructor[Thing.oneInstancePerWiki]) {
+        wikiData[thingConstructor[Thing.wikiData]] = thing;
+      } else {
+        wikiData[thingConstructor[Thing.wikiData]] = [thing];
+      }
+    }
+
     const documentEntries = Object.entries(document)
       .filter(([field]) => !ignoredFields.includes(field));
 
@@ -312,26 +348,29 @@ function makeProcessDocument(thingConstructor, {
     const followSubdocSetup = setup => {
       let error = null;
 
-      let subthing;
+      let result;
       try {
-        const result = bouncer(setup.data, setup.documentType);
-        subthing = result.thing;
-        result.aggregate.close();
+        let aggregate;
+        ({result, aggregate} = bouncer(setup.data, setup.documentType));
+        aggregate.close();
       } catch (caughtError) {
         error = caughtError;
       }
 
-      if (subthing) {
+      if (result.thing) {
         if (setup.bindInto) {
-          subthing[setup.bindInto] = thing;
+          result.thing[setup.bindInto] = thing;
         }
 
         if (setup.provide) {
-          Object.assign(subthing, setup.provide);
+          Object.assign(result.thing, setup.provide);
         }
       }
 
-      return {error, subthing};
+      pushWikiData(wikiData, result.wikiData);
+      flat.push(...result.flat);
+
+      return {error, subthing: result.thing};
     };
 
     for (const [field, layout] of Object.entries(subdocLayouts)) {
@@ -414,7 +453,14 @@ function makeProcessDocument(thingConstructor, {
             {preserveOriginalOrder: true})));
     }
 
-    return {thing, aggregate};
+    return {
+      aggregate,
+      result: {
+        thing,
+        flat,
+        wikiData,
+      },
+    };
   });
 }
 
@@ -1309,26 +1355,35 @@ export function processThingsFromDataStep(documents, dataStep) {
   switch (documentMode) {
     case documentModes.allInOne:
     case documentModes.allTogether: {
-      const result = [];
+      const things = [];
+      const flat = [];
+      const wikiData = {};
       const aggregate = openAggregate({message: `Errors processing documents`});
 
       documents.forEach(
         decorateErrorWithIndex((document, index) => {
-          const {thing, aggregate: subAggregate} =
+          const {result, aggregate: subAggregate} =
             processDocument(document, dataStep.documentThing);
 
-          thing[Thing.yamlSourceDocument] = document;
-          thing[Thing.yamlSourceDocumentPlacement] =
+          result.thing[Thing.yamlSourceDocument] = document;
+          result.thing[Thing.yamlSourceDocumentPlacement] =
             [documentModes.allInOne, index];
 
-          result.push(thing);
+          things.push(result.thing);
+          flat.push(...result.flat);
+          pushWikiData(wikiData, result.wikiData);
+
           aggregate.call(subAggregate.close);
         }));
 
       return {
         aggregate,
-        result,
-        things: result,
+        result: {
+          network: things,
+          flat: things,
+          file: things,
+          wikiData,
+        },
       };
     }
 
@@ -1336,17 +1391,21 @@ export function processThingsFromDataStep(documents, dataStep) {
       if (documents.length > 1)
         throw new Error(`Only expected one document to be present, got ${documents.length}`);
 
-      const {thing, aggregate} =
+      const {result, aggregate} =
         processDocument(documents[0], dataStep.documentThing);
 
-      thing[Thing.yamlSourceDocument] = documents[0];
-      thing[Thing.yamlSourceDocumentPlacement] =
+      result.thing[Thing.yamlSourceDocument] = documents[0];
+      result.thing[Thing.yamlSourceDocumentPlacement] =
         [documentModes.oneDocumentTotal];
 
       return {
         aggregate,
-        result: thing,
-        things: [thing],
+        result: {
+          network: result.thing,
+          flat: result.flat,
+          file: [result.thing],
+          wikiData: result.wikiData,
+        },
       };
     }
 
@@ -1358,14 +1417,17 @@ export function processThingsFromDataStep(documents, dataStep) {
         throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`);
 
       const aggregate = openAggregate({message: `Errors processing documents`});
+      const wikiData = {};
 
-      const {thing: headerThing, aggregate: headerAggregate} =
+      const {result: headerResult, aggregate: headerAggregate} =
         processDocument(headerDocument, dataStep.headerDocumentThing);
 
-      headerThing[Thing.yamlSourceDocument] = headerDocument;
-      headerThing[Thing.yamlSourceDocumentPlacement] =
+      headerResult.thing[Thing.yamlSourceDocument] = headerDocument;
+      headerResult.thing[Thing.yamlSourceDocumentPlacement] =
         [documentModes.headerAndEntries, 'header'];
 
+      pushWikiData(wikiData, headerResult.wikiData);
+
       try {
         headerAggregate.close();
       } catch (caughtError) {
@@ -1373,17 +1435,18 @@ export function processThingsFromDataStep(documents, dataStep) {
         aggregate.push(caughtError);
       }
 
-      const entryThings = [];
+      const entryResults = [];
 
       for (const [index, entryDocument] of entryDocuments.entries()) {
-        const {thing: entryThing, aggregate: entryAggregate} =
+        const {result: entryResult, aggregate: entryAggregate} =
           processDocument(entryDocument, dataStep.entryDocumentThing);
 
-        entryThing[Thing.yamlSourceDocument] = entryDocument;
-        entryThing[Thing.yamlSourceDocumentPlacement] =
+        entryResult.thing[Thing.yamlSourceDocument] = entryDocument;
+        entryResult.thing[Thing.yamlSourceDocumentPlacement] =
           [documentModes.headerAndEntries, 'entry', index];
 
-        entryThings.push(entryThing);
+        entryResults.push(entryResult);
+        pushWikiData(wikiData, entryResult.wikiData);
 
         try {
           entryAggregate.close();
@@ -1396,10 +1459,16 @@ export function processThingsFromDataStep(documents, dataStep) {
       return {
         aggregate,
         result: {
-          header: headerThing,
-          entries: entryThings,
+          network: {
+            header: headerResult.thing,
+            entries: entryResults.map(result => result.thing),
+          },
+
+          flat: headerResult.flat.concat(entryResults.flatMap(result => result.flat)),
+          file: [headerResult.thing, ...entryResults.map(result => result.thing)],
+
+          wikiData,
         },
-        things: [headerThing, ...entryThings],
       };
     }
 
@@ -1410,17 +1479,21 @@ export function processThingsFromDataStep(documents, dataStep) {
       if (empty(documents) || !documents[0])
         throw new Error(`Expected a document, this file is empty`);
 
-      const {thing, aggregate} =
+      const {result, aggregate} =
         processDocument(documents[0], dataStep.documentThing);
 
-      thing[Thing.yamlSourceDocument] = documents[0];
-      thing[Thing.yamlSourceDocumentPlacement] =
+      result.thing[Thing.yamlSourceDocument] = documents[0];
+      result.thing[Thing.yamlSourceDocumentPlacement] =
         [documentModes.onePerFile];
 
       return {
         aggregate,
-        result: thing,
-        things: [thing],
+        result: {
+          network: result.thing,
+          flat: result.flat,
+          file: [result.thing],
+          wikiData: result.wikiData,
+        },
       };
     }
 
@@ -1521,10 +1594,10 @@ export async function processThingsFromDataSteps(documentLists, fileLists, dataS
           file: files,
           documents: documentLists,
         }).map(({file, documents}) => {
-            const {result, aggregate, things} =
+            const {result, aggregate} =
               processThingsFromDataStep(documents, dataStep);
 
-            for (const thing of things) {
+            for (const thing of result.file) {
               thing[Thing.yamlSourceFilename] =
                 path.relative(dataPath, file)
                   .split(path.sep)
@@ -1551,45 +1624,35 @@ export async function processThingsFromDataSteps(documentLists, fileLists, dataS
           translucent: true,
         }).contain(await fileListPromise));
 
-  const thingLists =
+  const results =
     aggregate
       .receive(await Promise.all(dataStepPromises));
 
-  return {aggregate, result: thingLists};
+  return {aggregate, result: results};
 }
 
-// Flattens a list of *lists* of things for a given data step (each list
-// corresponding to one YAML file) into results to be saved on the final
-// wikiData object, routing thing lists into the step's save() function.
-export function saveThingsFromDataStep(thingLists, dataStep) {
+// Runs a data step's connect() function, if present, with representations
+// of the results from the YAML files, called "networks" - one network and
+// one call to .connect() per YAML file - in order to form data connections
+// (direct links) between related objects within a file.
+export function connectThingsFromDataStep(results, dataStep) {
   const {documentMode} = dataStep;
 
   switch (documentMode) {
-    case documentModes.allInOne: {
-      const things =
-        (empty(thingLists)
-          ? []
-          : thingLists[0]);
-
-      return dataStep.save(things);
-    }
-
-    case documentModes.oneDocumentTotal: {
-      const thing =
-        (empty(thingLists)
-          ? {}
-          : thingLists[0]);
-
-      return dataStep.save(thing);
+    case documentModes.oneDocumentTotal:
+    case documentModes.onePerFile: {
+      // These results are never connected.
+      return;
     }
 
-    case documentModes.allTogether: {
-      return dataStep.save(thingLists.flat());
-    }
+    case documentModes.allInOne:
+    case documentModes.allTogether:
+    case documentModes.headerAndEntries: {
+      for (const result of results) {
+        dataStep.connect?.(result.network);
+      }
 
-    case documentModes.headerAndEntries:
-    case documentModes.onePerFile: {
-      return dataStep.save(thingLists);
+      break;
     }
 
     default:
@@ -1597,60 +1660,71 @@ export function saveThingsFromDataStep(thingLists, dataStep) {
   }
 }
 
-// Flattens a list of *lists* of things for each data step (each list
-// corresponding to one YAML file) into the final wikiData object,
-// routing thing lists into each step's save() function.
-export function saveThingsFromDataSteps(thingLists, dataSteps) {
+export function connectThingsFromDataSteps(processThingResultLists, dataSteps) {
   const aggregate =
     openAggregate({
-      message: `Errors finalizing things from data files`,
+      message: `Errors connecting things from data files`,
       translucent: true,
     });
 
-  const wikiData = {};
-
   stitchArrays({
     dataStep: dataSteps,
-    thingLists: thingLists,
-  }).map(({dataStep, thingLists}) => {
+    processThingResults: processThingResultLists,
+  }).forEach(({dataStep, processThingResults}) => {
       try {
-        return saveThingsFromDataStep(thingLists, dataStep);
+        connectThingsFromDataStep(processThingResults, dataStep);
       } catch (caughtError) {
         const error = new Error(
-          `Error finalizing things for data step: ${colors.bright(dataStep.title)}`,
+          `Error connecting things for data step: ${colors.bright(dataStep.title)}`,
           {cause: caughtError});
 
         error[Symbol.for('hsmusic.aggregate.translucent')] = true;
 
         aggregate.push(error);
-
-        return null;
       }
-    })
-    .filter(Boolean)
-    .forEach(saveResult => {
-      for (const [saveKey, saveValue] of Object.entries(saveResult)) {
-        if (Object.hasOwn(wikiData, saveKey)) {
-          if (Array.isArray(wikiData[saveKey])) {
-            if (Array.isArray(saveValue)) {
-              wikiData[saveKey].push(...saveValue);
-            } else {
-              throw new Error(`${saveKey} already present, expected array of items to push`);
-            }
-          } else {
-            if (Array.isArray(saveValue)) {
-              throw new Error(`${saveKey} already present and not an array, refusing to overwrite`);
-            } else {
-              throw new Error(`${saveKey} already present, refusing to overwrite`);
-            }
-          }
-        } else {
-          wikiData[saveKey] = saveValue;
+    });
+
+  return {result: null, aggregate};
+}
+
+export function makeWikiDataFromDataSteps(processThingResultLists, _dataSteps) {
+  const wikiData = {};
+
+  let found = false;
+  for (const result of processThingResultLists.flat(2)) {
+    pushWikiData(wikiData, result.wikiData);
+  }
+
+  const scanForConstituted =
+    processThingResultLists.flat(2).flatMap(result => result.flat);
+
+  const exists = new Set(scanForConstituted);
+
+  while (scanForConstituted.length) {
+    const scanningThing = scanForConstituted.pop();
+
+    for (const key of scanningThing.constructor[Thing.constitutibleProperties] ?? []) {
+      const maybeConstitutedThings =
+        (Array.isArray(scanningThing[key])
+          ? scanningThing[key]
+       : scanningThing[key]
+          ? [scanningThing[key]]
+          : []);
+
+      for (const thing of maybeConstitutedThings) {
+        if (exists.has(thing)) continue;
+        exists.add(thing);
+
+        if (thing.constructor[Thing.wikiData]) {
+          pushWikiData(wikiData, {[thing.constructor[Thing.wikiData]]: [thing]});
         }
+
+        scanForConstituted.push(thing);
       }
-    });
+    }
+  }
 
-  return {aggregate, result: wikiData};
+  return wikiData;
 }
 
 export async function loadAndProcessDataDocuments(dataSteps, {dataPath}) {
@@ -1663,13 +1737,15 @@ export async function loadAndProcessDataDocuments(dataSteps, {dataPath}) {
     aggregate.receive(
       await loadYAMLDocumentsFromDataSteps(dataSteps, {dataPath}));
 
-  const thingLists =
+  const processThingResultLists =
     aggregate.receive(
       await processThingsFromDataSteps(documentLists, fileLists, dataSteps, {dataPath}));
 
+  aggregate.receive(
+    connectThingsFromDataSteps(processThingResultLists, dataSteps));
+
   const wikiData =
-    aggregate.receive(
-      saveThingsFromDataSteps(thingLists, dataSteps));
+    makeWikiDataFromDataSteps(processThingResultLists, dataSteps);
 
   return {aggregate, result: wikiData};
 }