]> Lady’s Gitweb - Etiquette/blobdiff - model.js
Give Tag constructors a superclass
[Etiquette] / model.js
index ba71361890494f3e3c7aaa4369af83d50b8e8440..d64f6c4218889f7cab3c60dbc215fec884fe6ead 100644 (file)
--- a/model.js
+++ b/model.js
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
+import { identity } from "./deps.js";
 import { Storage } from "./memory.js";
 import { taggingDiscoveryContext } from "./names.js";
 import { Storage } from "./memory.js";
 import { taggingDiscoveryContext } from "./names.js";
-
-// TODO: Move these somewhere else and allow for modification before
-// they are used, freezing them only once tags actually start being
-// constructed (probably on first call to the `TagSystem` initializer
-// for convenience).
-//
-// Or else make them properties of the tag system itself and ∼fully
-// modifiable.
-
-/**
- * Tag kinds which denote entity tags.
- *
- * ※ This object is not exposed.
- */
-const ENTITY_TAG_KINDS = new Set([
-  "EntityTag",
-  "CharacterTag",
-  "InanimateEntityTag",
-]);
-
-/**
- * Tag kinds which denote relationship tags.
- *
- * ※ This object is not exposed.
- */
-const RELATIONSHIP_TAG_KINDS = new Set([
-  "RelationshipTag",
-  "FamilialRelationship Tag",
-  "FriendshipTag",
-  "RivalryTag",
-  "RomanticRelationshipTag",
-  "SexualRelationshipTag",
-]);
-
-/**
- * Tag kinds which denote setting tags.
- *
- * ※ This object is not exposed.
- */
-const SETTING_TAG_KINDS = new Set([
-  "SettingTag",
-  "LocationTag",
-  "TimePeriodTag",
-  "UniverseTag",
-]);
-
-/**
- * Tag kinds which denote conceptual tags.
- *
- * ※ This object is not exposed.
- */
-const CONCEPTUAL_TAG_KINDS = new Set(function* () {
-  yield "ConceptualTag";
-  yield* RELATIONSHIP_TAG_KINDS;
-}());
-
-/**
- * All recognized tag kinds.
- *
- * ※ This object is not exposed.
- */
-const TAG_KINDS = new Set(function* () {
-  yield "Tag";
-  yield "CanonTag";
-  yield* CONCEPTUAL_TAG_KINDS;
-  yield* ENTITY_TAG_KINDS;
-  yield "GenreTag";
-  yield* SETTING_TAG_KINDS;
-}());
-
-/**
- * Tag kinds which can be in canon.
- *
- * ※ This object is not exposed.
- */
-const HAS_IN_CANON = new Set(function* () {
-  yield* ENTITY_TAG_KINDS;
-  yield* SETTING_TAG_KINDS;
-}());
-
-/**
- * Tag kinds which can be involved in relationship tags.
- *
- * ※ This object is not exposed.
- */
-const INVOLVABLE_IN_RELATIONSHIP = new Set(function* () {
-  yield "CharacterTag";
-  yield* RELATIONSHIP_TAG_KINDS;
-}());
-
-/**
- * Properties which take literal values instead of identifiers.
- *
- * These are the label terms.
- */
-const LITERAL_TERMS = new Set([
-  "prefLabel",
-  "altLabel",
-  "hiddenLabel",
-]);
-
-/**
- * Properties to skip when diffing.
- *
- * These are all inverses of properties included in diffs and cannot be
- * changed manually.
- */
-const SKIP_IN_DIFF = new Set([
-  "hasInCanon",
-  "isIncludedIn",
-  "narrower",
-]);
+import schema from "./schema.js";
 
 /**
  * A tag.
 
 /**
  * A tag.
@@ -143,6 +33,9 @@ class Tag {
   /** The `Storage` managed by this `Tag`’s `TagSystem`. */
   #storage;
 
   /** The `Storage` managed by this `Tag`’s `TagSystem`. */
   #storage;
 
+  /** The schema in use for this `Tag`. */
+  #schema;
+
   /**
    * The 30‐bit W·R·M·G base32 identifier with leading checksum which
    * has been assigned to this `Tag`.
   /**
    * The 30‐bit W·R·M·G base32 identifier with leading checksum which
    * has been assigned to this `Tag`.
@@ -167,318 +60,73 @@ class Tag {
   #data = tagData();
 
   /**
   #data = tagData();
 
   /**
-   * Returns whether or not the provided value is a tag which shares a
-   * storage with this tag.
-   *
-   * Sharing a storage also implies sharing a `TagSystem`.
-   */
-  #isTagInStorage($) {
-    try {
-      // Try to compare the provided value’s internal store with
-      // the provided storage.
-      return $.#storage == this.#storage;
-    } catch {
-      // The provided value was not a `Tag`.
-      return false;
-    }
-  }
-
-  /**
-   * Constructs a new `Tag` of the provided kind and with the provided
-   * preferred label.
-   *
-   * ※ The first two arguments of this constructor are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this constructor in its unbound form from outside this
-   * module.
-   *
-   * ☡ This constructor throws if the provided kind is not supported.
-   */
-  constructor(system, storage, kind = "Tag", prefLabel = "") {
-    const kindString = `${kind}`;
-    if (TAG_KINDS.has(kindString)) {
-      // The provided kind is one of the recognized tag kinds.
-      this.#system = system;
-      this.#storage = storage;
-      this.#kind = kindString;
-      this.#data.prefLabel = prefLabel;
-    } else {
-      // The provided kind is not supported.
-      throw new RangeError(
-        `Cannot construct Tag: Unrecognized kind: ${kind}.`,
-      );
-    }
-  }
-
-  /**
-   * Yields the tags in the `TagSystem` associated with this
-   * constructor.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   */
-  static *all(system, storage) {
-    for (const instance of storage.values()) {
-      // Iterate over the entries and yield the ones which are `Tag`s
-      // in this `TagSystem`.
-      if (Tag.getSystem(instance) == system) {
-        // The current instance is a `Tag` in this `TagSystem`.
-        yield instance;
-      } else {
-        // The current instance is not a `Tag` in this `TagSystem`.
-        /* do nothing */
-      }
-    }
-  }
-
-  /**
-   * Returns a new `Tag` resolved from the provided I·R·I.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   *
-   * ☡ This function throws if the I·R·I is not in the `.iriSpace` of
-   * the `TagSystem` associated with this constructor.
-   *
-   * ※ If the I·R·I is not recognized, this function returns `null`.
-   */
-  static fromIRI(system, storage, iri) {
-    const name = `${iri}`;
-    const prefix = `${system.iriSpace}`;
-    if (!name.startsWith(prefix)) {
-      // The I·R·I does not begin with the expected prefix.
-      throw new RangeError(
-        `I·R·I did not begin with the expected prefix: ${iri}`,
-      );
-    } else {
-      // The I·R·I begins with the expected prefix.
-      const identifier = name.substring(prefix.length);
-      try {
-        // Attempt to resolve the identifier.
-        const instance = storage.get(identifier);
-        return Tag.getSystem(instance) == system ? instance : null;
-      } catch {
-        // Do not throw for bad identifiers.
-        return null;
-      }
-    }
-  }
-
-  /**
-   * Returns a new `Tag` resolved from the provided identifier.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   *
-   * ☡ This function throws if the identifier is invalid.
-   *
-   * ※ If the identifier is valid but not recognized, this function
-   * returns `null`.
-   */
-  static fromIdentifier(system, storage, identifier) {
-    const instance = storage.get(identifier);
-    return Tag.getSystem(instance) == system ? instance : null;
-  }
-
-  /**
-   * Returns a new `Tag` resolved from the provided Tag U·R·I.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   *
-   * ☡ This function throws if the provided Tag U·R·I does not match
-   * the tagging entity of this constructor’s `TagSystem`.
-   *
-   * ※ If the specific component of the Tag U·R·I is not recognized,
-   * this function returns `null`.
-   */
-  static fromTagURI(system, storage, tagURI) {
-    const tagName = `${tagURI}`;
-    const tagPrefix = `tag:${system.taggingEntity}:`;
-    if (!tagName.startsWith(tagPrefix)) {
-      // The Tag U·R·I does not begin with the expected prefix.
-      throw new RangeError(
-        `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
-      );
-    } else {
-      // The I·R·I begins with the expected prefix.
-      const identifier = tagName.substring(tagPrefix.length);
-      try {
-        // Attempt to resolve the identifier.
-        const instance = storage.get(identifier);
-        return Tag.getSystem(instance) == system ? instance : null;
-      } catch {
-        // Do not throw for bad identifiers.
-        return null;
-      }
-    }
-  }
-
-  /**
-   * Returns the `TagSystem` that the provided value belongs to.
-   *
-   * ※ This function can be used to check if the provided value has
-   * private tag features.
-   *
-   * ※ This function is not exposed.
-   */
-  static getSystem($) {
-    return !(#system in Object($)) ? null : $.#system;
-  }
-
-  /**
-   * Yields the tag identifiers in the `TagSystem` associated with this
-   * constructor.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   */
-  static *identifiers(system, storage) {
-    for (const [identifier, instance] of storage.entries()) {
-      // Iterate over the entries and yield the ones which are `Tag`s
-      // in this `TagSystem`.
-      if (Tag.getSystem(instance) == system) {
-        // The current instance is a `Tag` in this `TagSystem`.
-        yield identifier;
-      } else {
-        // The current instance is not a `Tag` in this `TagSystem`.
-        /* do nothing */
-      }
-    }
-  }
-
-  /**
-   * Returns a new `Tag` constructed from the provided data and with
-   * the provided identifier.
-   *
-   * ※ This function will not work if called directly from `Tag` (and
-   * nor is it available *to* be called as such from outside this
-   * module). It must be called from a `TagSystem::Tag` bound
-   * constructor.
-   *
-   * ※ This function is not really intended for public usage.
+   * Adds the provided label(s) to this `Tag` as the provided
+   * predicate, then returns this `Tag`.
    */
    */
-  static [Storage.toInstance](_system, _storage, data, identifier) {
-    const tag = new this(data.kind);
-    tag.#identifier = `${identifier}`;
-    tag.#persistedData = tagData(data);
-    tag.#data = tagData(data);
-    return tag;
-  }
-
-  static {
-    // Overwrite the default `::constructor` method to instead give the
-    // actual (bound) constructor which was used to generate a given
-    // `Tag`.
-    Object.defineProperties(this.prototype, {
-      constructor: {
-        configurable: true,
-        enumerable: false,
-        get() {
-          // All `Tag`s are constructed via the `.Tag` constructor
-          // available in their `TagSystem`; return it.
-          return this.#system.Tag;
-        },
-        set: undefined,
-      },
-    });
-  }
-
-  /**
-   * Adds the provided label(s) to this `Tag` as alternate labels, then
-   * returns this `Tag`.
-   */
-  addAltLabel(...labels) {
-    const altLabels = this.#data.altLabel;
-    let objectLabels = null; // initialized on first use
+  #addLabel(predicate, ...labels) {
+    const values = this.#data[predicate];
     for (const $ of labels) {
       // Iterate over each provided label and attempt to add it.
       const literal = langString($);
     for (const $ of labels) {
       // Iterate over each provided label and attempt to add it.
       const literal = langString($);
-      if (Object(literal) === literal) {
-        // The current label is a language‐tagged string.
-        objectLabels ??= [...function* () {
-          for (const altLabel of altLabels) {
-            // Iterate over the existing labels and yield the
-            // language‐tagged strings.
-            if (Object(altLabel) === altLabel) {
-              // The current existing label is a language‐tagged
-              // string.
-              yield altLabel;
-            } else {
-              // The current existing label is not a language‐tagged
-              // string.
-              /* do nothing */
-            }
-          }
-        }()];
-        if (
-          objectLabels.some((objectLabel) =>
-            objectLabel["@value"] == literal["@value"] &&
-            objectLabel["@language"] == literal["@language"]
-          )
-        ) {
-          // There is a match with the current label in the existing
-          // labels.
-          /* do nothing */
-        } else {
-          // There is no match and this label must be added.
-          altLabels.add(literal);
-          objectLabels.push(literal);
-        }
-      } else {
-        // The current label is a simple string.
-        altLabels.add(literal);
-      }
+      values.add(literal);
     }
     return this;
   }
 
   /**
    * Adds the provided tags to the list of tags that this `Tag` is
     }
     return this;
   }
 
   /**
    * Adds the provided tags to the list of tags that this `Tag` is
-   * narrower than, then returns this `Tag`.
+   * related to by the provided predicate, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
    */
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
    */
-  addBroaderTag(...tags) {
+  #addTag(predicate, ...tags) {
     const storage = this.#storage;
     const storage = this.#storage;
-    const broader = this.#data.broader;
+    const values = this.#data[predicate];
     for (const $ of tags) {
     for (const $ of tags) {
-      // Iterate over each tag and attempt to set it as broader than
-      // this `Tag`.
+      // Iterate over each tag and attempt to state the predicate.
       const identifier = toIdentifier($);
       if (identifier == null) {
         // ☡ The current tag has no identifier.
         throw new TypeError(
       const identifier = toIdentifier($);
       if (identifier == null) {
         // ☡ The current tag has no identifier.
         throw new TypeError(
-          "Cannot assign broader to Tag: Identifier must not be nullish.",
+          `Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
         );
         );
-      } else if (broader.has(identifier)) {
-        // Short‐circuit: The identifier is already something this
-        // `Tag` is narrower than.
+      } else if (values.has(identifier)) {
+        // Short‐circuit: The identifier has already been stated with
+        // this predicate.
         /* do nothing */
       } else {
         /* do nothing */
       } else {
-        // The current tag has an identifier.
+        // The current tag has an identifier, but it hasn’t been stated
+        // with this predicate yet.
         const tag = storage.get(identifier);
         if (tag == null) {
           // ☡ The current tag has not been persisted to this `Tag`’s
           // storage.
           throw new RangeError(
         const tag = storage.get(identifier);
         if (tag == null) {
           // ☡ The current tag has not been persisted to this `Tag`’s
           // storage.
           throw new RangeError(
-            `Cannot assign broader to Tag: Identifier is not persisted: ${identifier}.`,
+            `Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`,
           );
         } else if (!this.#isTagInStorage(tag)) {
           // ☡ The current tag is not a tag in the correct tag system.
           throw new TypeError(
           );
         } else if (!this.#isTagInStorage(tag)) {
           // ☡ The current tag is not a tag in the correct tag system.
           throw new TypeError(
-            `Cannot assign broader to Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
+            `Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
+          );
+        } else if (
+          !isObjectPredicateOK(
+            this.#schema,
+            this.#kind,
+            predicate,
+            tag.#kind,
+          )
+        ) {
+          // ☡ This tag and the current tag form an invalid pair for
+          // this predicate.
+          throw new TypeError(
+            `Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`,
           );
         } else {
           // The current tag is a tag in the correct tag system; add
           // its identifier.
           );
         } else {
           // The current tag is a tag in the correct tag system; add
           // its identifier.
-          broader.add(identifier);
+          values.add(identifier);
         }
       }
     }
         }
       }
     }
@@ -486,253 +134,134 @@ class Tag {
   }
 
   /**
   }
 
   /**
-   * Adds the provided label(s) to this `Tag` as hidden labels, then
-   * returns this `Tag`.
+   * Removes the provided string label(s) from this `Tag` as the
+   * provided predicate, then returns this `Tag`.
    */
    */
-  addHiddenLabel(...labels) {
-    const hiddenLabels = this.#data.hiddenLabel;
-    let objectLabels = null; // initialized on first use
+  #deleteLabel(predicate, ...labels) {
+    const values = this.#data[predicate];
     for (const $ of labels) {
     for (const $ of labels) {
-      // Iterate over each provided label and attempt to add it.
+      // Iterate over each provided label and attempt to remove it.
       const literal = langString($);
       const literal = langString($);
-      if (Object(literal) === literal) {
-        // The current label is a language‐tagged string.
-        objectLabels ??= [...function* () {
-          for (const hiddenLabel of hiddenLabels) {
-            // Iterate over the existing labels and yield the
-            // language‐tagged strings.
-            if (Object(hiddenLabel) === hiddenLabel) {
-              // The current existing label is a language‐tagged
-              // string.
-              yield hiddenLabel;
-            } else {
-              // The current existing label is not a language‐tagged
-              // string.
-              /* do nothing */
-            }
-          }
-        }()];
-        if (
-          objectLabels.some((objectLabel) =>
-            objectLabel["@value"] == literal["@value"] &&
-            objectLabel["@language"] == literal["@language"]
-          )
-        ) {
-          // There is a match with the current label in the existing
-          // labels.
-          /* do nothing */
-        } else {
-          // There is no match and this label must be added.
-          hiddenLabels.add(literal);
-          objectLabels.push(literal);
-        }
-      } else {
-        // The current label is a simple string.
-        hiddenLabels.add(literal);
-      }
+      values.delete(literal);
     }
     return this;
   }
 
   /**
     }
     return this;
   }
 
   /**
-   * Adds the provided tags to the list of tags that this `Tag` is in
-   * canon with, then returns this `Tag`.
+   * Removes the provided tags from the list of tags that this `Tag` is
+   * related to by the provided predicate, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
-   *
-   * ☡ This method will throw if a provided argument does not indicate
-   * a canon tag, or if this `Tag` is not of a kind which can be placed
-   * in canon.
    */
    */
-  addInCanonTag(...tags) {
-    const storage = this.#storage;
-    const kind = this.#kind;
-    const inCanon = this.#data.inCanon;
-    if (!HAS_IN_CANON.has(kind)) {
-      // ☡ This is not an entity tag, setting tag, or recognized
-      // subclass.
-      throw new TypeError(
-        `Cannot put Tag in canon: Incorrect Tag type: ${kind}.`,
-      );
-    } else {
-      // This has a kind which can be placed in canon.
-      for (const $ of tags) {
-        // Iterate over each tag and attempt to set this `Tag` in canon
-        // of it.
-        const identifier = toIdentifier($);
-        if (identifier == null) {
-          // ☡ The current tag has no identifier.
-          throw new TypeError(
-            "Cannot put Tag in canon: Identifier must not be nullish.",
-          );
-        } else if (inCanon.has(identifier)) {
-          // Short‐circuit: The identifier is already something this
-          // `Tag` is in canon of.
-          /* do nothing */
-        } else {
-          // The current tag has an identifier.
-          const tag = storage.get(identifier);
-          if (tag == null) {
-            // ☡ The current tag has not been persisted to this `Tag`’s
-            // storage.
-            throw new RangeError(
-              `Cannot put Tag in canon: Identifier is not persisted: ${identifier}.`,
-            );
-          } else if (
-            // ※ If the first check succeeds, then the current tag
-            // must have `Tag` private class features.
-            !this.#isTagInStorage(tag) || tag.#kind != "CanonTag"
-          ) {
-            // ☡ The current tag is not a canon tag in the correct
-            // tag system.
-            throw new TypeError(
-              `Cannot put Tag in canon: Tags can only be in Canon Tags from the same Tag System, but got: ${identifier}.`,
-            );
-          } else {
-            // The current tag is a canon tag in the correct tag
-            // system; add its identifier.
-            inCanon.add(identifier);
-          }
-        }
-      }
+  #deleteTag(predicate, ...tags) {
+    const values = this.#data[predicate];
+    for (const $ of tags) {
+      // Iterate over the provided tags and delete them.
+      values.delete(toIdentifier($));
     }
     return this;
   }
 
   /**
     }
     return this;
   }
 
   /**
-   * Adds the provided tags to the list of tags that this `Tag`
-   * involves, then returns this `Tag`.
-   *
-   * Arguments may be string identifiers or objects with an
-   * `.identifier` property.
+   * Returns whether or not the provided value is a tag which shares a
+   * storage with this tag.
    *
    *
-   * ☡ This method will throw if this `Tag` is not a conceptual tag, or
-   * if this `Tag` is a relationship tag and a provided argument does
-   * not indicate a character or relationship tag.
+   * Sharing a storage also implies sharing a `TagSystem`.
    */
    */
-  addInvolvesTag(...tags) {
-    const storage = this.#storage;
-    const kind = this.#kind;
-    const involves = this.#data.involves;
-    if (!CONCEPTUAL_TAG_KINDS.has(kind)) {
-      // ☡ This is not a conceptual tag or recognized subclass.
-      throw new TypeError(
-        `Cannot involve Tag: Incorrect Tag type: ${kind}.`,
-      );
-    } else {
-      // This is a conceptual tag.
-      for (const $ of tags) {
-        // Iterate over each tag and attempt to set this `Tag` as
-        // involving it.
-        const identifier = toIdentifier($);
-        if (identifier == null) {
-          // ☡ The current tag has no identifier.
-          throw new TypeError(
-            "Cannot involve Tag: Identifier must not be nullish.",
-          );
-        } else if (involves.has(identifier)) {
-          // Short‐circuit: The identifier is already something this
-          // `Tag` involves.
-          /* do nothing */
-        } else {
-          // The current tag has an identifier.
-          const tag = storage.get(identifier);
-          if (tag == null) {
-            // ☡ The current tag has not been persisted to this `Tag`’s
-            // storage.
-            throw new RangeError(
-              `Cannot involve Tag: Identifier is not persisted: ${identifier}.`,
-            );
-          } else if (
-            // ※ If the first check succeeds, then the current tag
-            // must have `Tag` private class features.
-            !this.#isTagInStorage(tag) ||
-            RELATIONSHIP_TAG_KINDS.has(kind) &&
-              !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind)
-          ) {
-            // ☡ The current tag is in the correct tag system and
-            // includable.
-            throw new TypeError(
-              `Cannot involve Tag: Tags must be the same Tag System and involvable, but got: ${identifier}.`,
-            );
-          } else {
-            // The current tag is an involvable tag in the correct tag
-            // system; add its identifier.
-            involves.add(identifier);
-          }
-        }
-      }
+  #isTagInStorage($) {
+    try {
+      // Try to compare the provided value’s internal store with
+      // the provided storage.
+      return $.#storage == this.#storage;
+    } catch {
+      // The provided value was not a `Tag`.
+      return false;
     }
     }
-    return this;
   }
 
   }
 
-  /** Yields the alternative labels of this `Tag`. */
-  *altLabels() {
-    yield* this.#data.altLabel;
-  }
-
-  /** Returns the authority (domain) name for this `Tag`. */
-  get authorityName() {
-    return this.#system.authorityName;
+  /**
+   * Yields the labels of this `Tag` according to the provided
+   * predicate.
+   */
+  *#yieldLabels(predicate) {
+    yield* this.#data[predicate];
   }
 
   }
 
-  /** Yields `Tag`s which are broader than this `Tag`. */
-  *broaderTags() {
+  /**
+   * Yields the tags that this `Tag` is related to by the provided
+   * predicate.
+   */
+  *#yieldTags(predicate) {
     const storage = this.#storage;
     const storage = this.#storage;
-    for (const identifier of this.#data.broader) {
-      // Iterate over the broader tags and yield them if possible.
+    for (const identifier of this.#data[predicate]) {
+      // Iterate over the tags in this predicate and yield them if
+      // possible.
       const tag = storage.get(identifier);
       const tag = storage.get(identifier);
-      if (!this.#isTagInStorage(tag)) {
-        // The broader tag no longer appears in storage; perhaps it was
-        // deleted.
+      if (
+        !this.#isTagInStorage(tag) || !isObjectPredicateOK(
+          this.#schema,
+          this.#kind,
+          predicate,
+          tag.#kind,
+        )
+      ) {
+        // The tag no longer appears in storage or is not compatible;
+        // perhaps it was deleted.
         /* do nothing */
       } else {
         /* do nothing */
       } else {
-        // The broader tag exists and is constructable from storage.
+        // The tag exists and is constructable from storage.
         yield tag;
       }
     }
   }
 
         yield tag;
       }
     }
   }
 
-  /** Yields `Tag`s which are broader than this `Tag`, transitively. */
-  *broaderTransitiveTags() {
+  /**
+   * Yields the tags that this `Tag` is related to by the provided
+   * predicate, figured transitively.
+   */
+  *#yieldTransitiveTags(transitivePredicate, basePredicate) {
     const storage = this.#storage;
     const encountered = new Set();
     const storage = this.#storage;
     const encountered = new Set();
-    let pending = new Set(this.#data.broader);
+    let pending = new Set(this.#data[basePredicate]);
     while (pending.size > 0) {
     while (pending.size > 0) {
-      // Loop until all broader tags have been encountered.
+      // Loop until all tags of the predicate have been encountered.
       const processing = pending;
       pending = new Set();
       for (const identifier of processing) {
       const processing = pending;
       pending = new Set();
       for (const identifier of processing) {
-        // Iterate over the broader tags and yield them if possible.
+        // Iterate over the tags and yield them if possible.
         if (!encountered.has(identifier)) {
         if (!encountered.has(identifier)) {
-          // The broader tag has not been encountered before.
+          // The tag has not been encountered before.
           encountered.add(identifier);
           const tag = storage.get(identifier);
           encountered.add(identifier);
           const tag = storage.get(identifier);
-          if (!this.#isTagInStorage(tag)) {
-            // The broader tag no longer appears in storage; perhaps it
-            // was deleted.
+          if (
+            !this.#isTagInStorage(tag) || !isObjectPredicateOK(
+              this.#schema,
+              this.#kind,
+              transitivePredicate,
+              tag.#kind,
+            )
+          ) {
+            // The tag no longer appears in storage or is not
+            // compatible; perhaps it was deleted.
             /* do nothing */
           } else {
             /* do nothing */
           } else {
-            // The broader tag exists and is constructable from
-            // storage.
+            // The tag exists and is constructable from storage.
             yield tag;
             yield tag;
-            for (const transitive of tag.#data.broader) {
-              // Iterate over the broader tags of the current broader
-              // tag and add them to pending as needed.
+            for (const transitive of tag.#data[basePredicate]) {
+              // Iterate over the nested tags of the current tag and
+              // add them to pending as needed.
               if (!encountered.has(transitive)) {
               if (!encountered.has(transitive)) {
-                // The broader broader tag has not been encountered
-                // yet.
+                // The nested tag has not been encountered yet.
                 pending.add(transitive);
               } else {
                 pending.add(transitive);
               } else {
-                // The broader broader tag has already been
-                // encountered.
+                // The nested tag has already been encountered.
                 /* do nothing */
               }
             }
           }
         } else {
                 /* do nothing */
               }
             }
           }
         } else {
-          // The broader tag has already been encountered.
+          // The tag has already been encountered.
           /* do nothing */
         }
       }
           /* do nothing */
         }
       }
@@ -740,160 +269,203 @@ class Tag {
   }
 
   /**
   }
 
   /**
-   * Removes the provided string label(s) from this `Tag` as alternate
-   * labels, then returns this `Tag`.
-   */
-  deleteAltLabel(...labels) {
-    const altLabels = this.#data.altLabel;
-    let objectLabels = null; // initialized on first use
-    for (const $ of labels) {
-      // Iterate over each provided label and attempt to remove it.
-      const literal = langString($);
-      if (Object(literal) === literal) {
-        // The current label is a language‐tagged string.
-        objectLabels ??= [...function* () {
-          for (const altLabel of altLabels) {
-            // Iterate over the existing labels and yield the
-            // language‐tagged strings.
-            if (Object(altLabel) === altLabel) {
-              // The current existing label is a language‐tagged
-              // string.
-              yield altLabel;
-            } else {
-              // The current existing label is not a language‐tagged
-              // string.
-              /* do nothing */
-            }
-          }
-        }()];
-        const existing = objectLabels.find((objectLabel) =>
-          objectLabel["@value"] == literal["@value"] &&
-          objectLabel["@language"] == literal["@language"]
-        );
-        altLabels.delete(existing);
-      } else {
-        // The current label is a simple string.
-        altLabels.delete(literal);
-      }
-    }
-    return this;
-  }
-
-  /**
-   * Removes the provided tags from the list of tags that this `Tag` is
-   * narrower than, then returns this `Tag`.
+   * Constructs a new `Tag` of the provided kind and with the provided
+   * preferred label.
    *
    *
-   * Arguments may be string identifiers or objects with an
-   * `.identifier` property.
+   * ※ The first two arguments of this constructor are bound when
+   * generating the value of `TagSystem::Tag`. It isn’t possible to
+   * access this constructor in its unbound form from outside this
+   * module.
+   *
+   * ☡ This constructor throws if the provided kind is not supported.
    */
    */
-  deleteBroaderTag(...tags) {
-    const broader = this.#data.broader;
-    for (const $ of tags) {
-      // Iterate over the provided tags and delete them.
-      broader.delete(toIdentifier($));
+  constructor(system, storage, schema, kind = "Tag", prefLabel = "") {
+    this.#system = system;
+    this.#storage = storage;
+    this.#schema = schema;
+    const kindString = `${kind}`;
+    if (!(kindString in schema.classes)) {
+      // The provided kind is not supported.
+      throw new RangeError(
+        `Cannot construct Tag: Unrecognized kind: ${kind}.`,
+      );
+    } else {
+      // The provided kind is one of the recognized tag kinds.
+      this.#kind = kindString;
+      this.#data.prefLabel = prefLabel;
     }
     }
-    return this;
   }
 
   /**
   }
 
   /**
-   * Removes the provided string label(s) from this `Tag` as hidden
-   * labels, then returns this `Tag`.
+   * Returns a new `Tag` constructor for the provided system, storage,
+   * schema, created with an appropriate prototype for the properties
+   * so defined.
+   *
+   * ※ This function is not exposed.
    */
    */
-  deleteHiddenLabel(...labels) {
-    const hiddenLabels = this.#data.hiddenLabel;
-    let objectLabels = null; // initialized on first use
-    for (const $ of labels) {
-      // Iterate over each provided label and attempt to remove it.
-      const literal = langString($);
-      if (Object(literal) === literal) {
-        // The current label is a language‐tagged string.
-        objectLabels ??= [...function* () {
-          for (const hiddenLabel of hiddenLabels) {
-            // Iterate over the existing labels and yield the
-            // language‐tagged strings.
-            if (Object(hiddenLabel) === hiddenLabel) {
-              // The current existing label is a language‐tagged
-              // string.
-              yield hiddenLabel;
-            } else {
-              // The current existing label is not a language‐tagged
-              // string.
-              /* do nothing */
-            }
-          }
-        }()];
-        const existing = objectLabels.find((objectLabel) =>
-          objectLabel["@value"] == literal["@value"] &&
-          objectLabel["@language"] == literal["@language"]
-        );
-        hiddenLabels.delete(existing);
-      } else {
-        // The current label is a simple string.
-        hiddenLabels.delete(literal);
-      }
-    }
-    return this;
+  static For(system, storage, schema) {
+    const {
+      objectProperties,
+      transitiveProperties,
+      dataProperties,
+    } = schema;
+    const constructor = function (...$s) {
+      return Reflect.construct(
+        Tag,
+        [system, storage, schema, ...$s],
+        new.target,
+      );
+    };
+    Object.defineProperties(constructor, {
+      name: { value: "TagSystem::Tag" },
+      prototype: {
+        configurable: false,
+        enumerable: false,
+        value: Object.create(
+          Tag.prototype,
+          Object.fromEntries(Array.from(
+            function* () {
+              for (const key in objectProperties) {
+                // Iterate over each object property and yield any
+                // necessary method definitions.
+                const {
+                  inverseOf,
+                  subPropertyOf,
+                } = objectProperties[key];
+                if (key in transitiveProperties) {
+                  // The current key indicates a transitive property.
+                  //
+                  // Transitive property methods are added by their
+                  // nontransitive subproperties.
+                  /* do nothing */
+                } else {
+                  // The current key does not indicate a transitive
+                  // property.
+                  yield [`${key}Tags`, function* () {
+                    yield* this.#yieldTags(key);
+                  }];
+                  if (inverseOf == null) {
+                    // The current key does not indicate an inverse
+                    // property, so add and delete methods are also
+                    // added.
+                    const cased = key[0].toUpperCase() +
+                      key.substring(1);
+                    yield [`add${cased}Tag`, function (...tags) {
+                      return this.#addTag(key, ...tags);
+                    }];
+                    yield [`delete${cased}Tag`, function (...tags) {
+                      return this.#deleteTag(key, ...tags);
+                    }];
+                  } else {
+                    // The current key indicates an inverse property,
+                    // so no add and delete methods are necessary.
+                    /* do nothing */
+                  }
+                  if (
+                    subPropertyOf != null &&
+                    subPropertyOf in transitiveProperties
+                  ) {
+                    // The current key indicates a subproperty of a
+                    // transitive property; its method is also added.
+                    yield [`${subPropertyOf}Tags`, function* () {
+                      yield* this.#yieldTransitiveTags(
+                        subPropertyOf,
+                        key,
+                      );
+                    }];
+                  } else {
+                    // The current key does not indicate a subproperty
+                    // of a transitive property.
+                    /* do nothing */
+                  }
+                }
+              }
+              for (const key in dataProperties) {
+                // Iterate over each data property and yield any
+                // necessary method definitions.
+                if (key != "prefLabel") {
+                  // The current key is not `"prefLabel"`.
+                  const cased = key[0].toUpperCase() +
+                    key.substring(1);
+                  yield [`${key}s`, function* () {
+                    yield* this.#yieldLabels(key);
+                  }];
+                  yield [`add${cased}`, function (...labels) {
+                    return this.#addLabel(key, ...labels);
+                  }];
+                  yield [`delete${cased}`, function (...labels) {
+                    return this.#deleteLabel(key, ...labels);
+                  }];
+                } else {
+                  // The current key is `"prefLabel"`. This is a
+                  // special case which is not handled by the schema.
+                  /* do nothing */
+                }
+              }
+            }(),
+            ([key, value]) => [key, {
+              configurable: true,
+              enumerable: false,
+              value: Object.defineProperty(value, "name", {
+                value: key,
+              }),
+              writable: true,
+            }],
+          )),
+        ),
+        writable: false,
+      },
+    });
+    return new TagConstructor(constructor, system, storage, schema);
   }
 
   /**
   }
 
   /**
-   * Removes the provided tags from the list of tags that this `Tag` is
-   * in canon with, then returns this `Tag`.
+   * Assigns the provided data and identifier to the provided tag.
    *
    *
-   * Arguments may be string identifiers or objects with an
-   * `.identifier` property.
+   * ☡ This function throws if the provided tag is not a `Tag`.
+   *
+   * ※ This function is not exposed.
    */
    */
-  deleteInCanonTag(...tags) {
-    const inCanon = this.#data.inCanon;
-    for (const $ of tags) {
-      // Iterate over the provided tags and delete them.
-      inCanon.delete(toIdentifier($));
-    }
-    return this;
+  static assignData(tag, data, identifier) {
+    tag.#identifier = `${identifier}`;
+    tag.#persistedData = tagData(data);
+    tag.#data = tagData(data);
+    return tag;
   }
 
   /**
   }
 
   /**
-   * Removes the provided tags from the list of tags that this `Tag`
-   * involves, then returns this `Tag`.
+   * Returns the `TagSystem` that the provided value belongs to.
    *
    *
-   * Arguments may be string identifiers or objects with an
-   * `.identifier` property.
+   * ※ This function can be used to check if the provided value has
+   * private tag features.
+   *
+   * ※ This function is not exposed.
    */
    */
-  deleteInvolvesTag(...tags) {
-    const involves = this.#data.involves;
-    for (const $ of tags) {
-      // Iterate over the provided tags and delete them.
-      involves.delete(toIdentifier($));
-    }
-    return this;
+  static getSystem($) {
+    return !(#system in Object($)) ? null : $.#system;
   }
 
   }
 
-  /** Yields `Tag`s that are in canon of this `Tag`. */
-  *hasInCanonTags() {
-    const storage = this.#storage;
-    if (this.#kind == "CanonTag") {
-      // This is a canon tag.
-      for (const identifier of this.#data.hasInCanon) {
-        // Iterate over the tags in canon and yield them if possible.
-        const tag = storage.get(identifier);
-        if (
-          !this.#isTagInStorage(tag) || !HAS_IN_CANON.has(tag.#kind)
-        ) {
-          // The tag in canon no longer appears in storage; perhaps it
-          // was deleted.
-          /* do nothing */
-        } else {
-          // The tag in canon exists and is constructable from storage.
-          yield tag;
-        }
-      }
-    } else {
-      /* do nothing */
-    }
+  static {
+    // Overwrite the default `::constructor` method to instead give the
+    // actual (bound) constructor which was used to generate a given
+    // `Tag`.
+    Object.defineProperties(this.prototype, {
+      constructor: {
+        configurable: true,
+        enumerable: false,
+        get() {
+          // All `Tag`s are constructed via the `.Tag` constructor
+          // available in their `TagSystem`; return it.
+          return this.#system.Tag;
+        },
+        set: undefined,
+      },
+    });
   }
 
   }
 
-  /** Yields the hidden labels of this `Tag`. */
-  *hiddenLabels() {
-    yield* this.#data.hiddenLabel;
+  /** Returns the authority (domain) name for this `Tag`. */
+  get authorityName() {
+    return this.#system.authorityName;
   }
 
   /** Returns the identifier of this `Tag`. */
   }
 
   /** Returns the identifier of this `Tag`. */
@@ -901,80 +473,6 @@ class Tag {
     return this.#identifier;
   }
 
     return this.#identifier;
   }
 
-  /** Yields `Tag`s that this `Tag` is in canon of. */
-  *inCanonTags() {
-    const storage = this.#storage;
-    if (HAS_IN_CANON.has(this.#kind)) {
-      // This tag can be placed in canon.
-      for (const identifier of this.#data.inCanon) {
-        // Iterate over the canon tags and yield them if possible.
-        const tag = storage.get(identifier);
-        if (!this.#isTagInStorage(tag) || tag.#kind != "CanonTag") {
-          // The canon tag no longer appears in storage; perhaps it was
-          // deleted.
-          /* do nothing */
-        } else {
-          // The canon tag exists and is constructable from storage.
-          yield tag;
-        }
-      }
-    } else {
-      // This tag cannot be placed in canon.
-      /* do nothing */
-    }
-  }
-
-  /** Yields `Tag`s which involve this `Tag`. */
-  *involvedInTags() {
-    const storage = this.#storage;
-    for (const identifier of this.#data.involvedIn) {
-      // Iterate over the involving tags and yield them if possible.
-      const tag = storage.get(identifier);
-      const tagKind = tag.#kind;
-      if (
-        !this.#isTagInStorage(tag) ||
-        !CONCEPTUAL_TAG_KINDS.has(tagKind) ||
-        RELATIONSHIP_TAG_KINDS.has(tagKind) &&
-          !INVOLVABLE_IN_RELATIONSHIP.has(this.#kind)
-      ) {
-        // The including tag no longer appears in storage; perhaps it
-        // was deleted.
-        /* do nothing */
-      } else {
-        // The including tag exists and is constructable from storage.
-        yield tag;
-      }
-    }
-  }
-
-  /** Yields `Tag`s that this `Tag` involves. */
-  *involvesTags() {
-    const storage = this.#storage;
-    const kind = this.#kind;
-    if (CONCEPTUAL_TAG_KINDS.has(kind)) {
-      // This tag can involve other tags.
-      for (const identifier of this.#data.involves) {
-        // Iterate over the involved and yield them if possible.
-        const tag = storage.get(identifier);
-        if (
-          !this.#isTagInStorage(tag) ||
-          RELATIONSHIP_TAG_KINDS.has(kind) &&
-            !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind)
-        ) {
-          // The involved tag no longer appears in storage; perhaps it
-          // was deleted.
-          /* do nothing */
-        } else {
-          // The involved tag exists and is constructable from storage.
-          yield tag;
-        }
-      }
-    } else {
-      // This tag cannot involve other tags.
-      /* do nothing */
-    }
-  }
-
   /** Returns the I·R·I for this `Tag`. */
   get iri() {
     const { identifier, iriSpace } = this;
   /** Returns the I·R·I for this `Tag`. */
   get iri() {
     const { identifier, iriSpace } = this;
@@ -991,69 +489,6 @@ class Tag {
     return this.#kind;
   }
 
     return this.#kind;
   }
 
-  /** Yields `Tag`s which are narrower than this `Tag`. */
-  *narrowerTags() {
-    const storage = this.#storage;
-    for (const identifier of this.#data.narrower) {
-      const tag = storage.get(identifier);
-      if (!this.#isTagInStorage(tag)) {
-        // The narrower tag no longer appears in storage; perhaps it
-        // was deleted.
-        /* do nothing */
-      } else {
-        // The narrower tag exists and is constructable from storage.
-        yield tag;
-      }
-    }
-  }
-
-  /**
-   * Yields `Tag`s which are narrower than this `Tag`, transitively.
-   */
-  *narrowerTransitiveTags() {
-    const storage = this.#storage;
-    const encountered = new Set();
-    let pending = new Set(this.#data.narrower);
-    while (pending.size > 0) {
-      // Loop until all narrower tags have been encountered.
-      const processing = pending;
-      pending = new Set();
-      for (const identifier of processing) {
-        // Iterate over the narrower tags and yield them if possible.
-        if (!encountered.has(identifier)) {
-          // The narrower tag has not been encountered before.
-          encountered.add(identifier);
-          const tag = storage.get(identifier);
-          if (!this.#isTagInStorage(tag)) {
-            // The narrower tag no longer appears in storage; perhaps
-            // it was deleted.
-            /* do nothing */
-          } else {
-            // The narrower tag exists and is constructable from
-            // storage.
-            yield tag;
-            for (const transitive of tag.#data.narrower) {
-              // Iterate over the narrower tags of the current narrower
-              // tag and add them to pending as needed.
-              if (!encountered.has(transitive)) {
-                // The narrower narrower tag has not been encountered
-                // yet.
-                pending.add(transitive);
-              } else {
-                // The narrower narrower tag has already been
-                // encountered.
-                /* do nothing */
-              }
-            }
-          }
-        } else {
-          // The narrower tag has already been encountered.
-          /* do nothing */
-        }
-      }
-    }
-  }
-
   /**
    * Persist this `Tag` to storage and return an ActivityStreams
    * serialization of a Tag Activity representing any changes, or
   /**
    * Persist this `Tag` to storage and return an ActivityStreams
    * serialization of a Tag Activity representing any changes, or
@@ -1067,19 +502,27 @@ class Tag {
    * as broader than another causes the other tag to reciprocally be
    * marked as narrower.
    *
    * as broader than another causes the other tag to reciprocally be
    * marked as narrower.
    *
-   * ※ The inverse terms `hasInCanon`, `isIncludedIn`, and `narrower`
-   * will never appear in the predicates of generated activities.
+   * ※ Inverse object properties will never appear in the predicates
+   * of generated activities.
    */
   persist(silent = false) {
     const system = this.#system;
     const storage = this.#storage;
    */
   persist(silent = false) {
     const system = this.#system;
     const storage = this.#storage;
+    const {
+      objectProperties,
+      transitiveProperties,
+      dataProperties,
+    } = this.#schema;
     const persistedData = this.#persistedData;
     const data = this.#data;
     const diffs = {};
     for (const [key, value] of Object.entries(data)) {
       // Iterate over each entry of the tag data and create a diff
       // with the last persisted information.
     const persistedData = this.#persistedData;
     const data = this.#data;
     const diffs = {};
     for (const [key, value] of Object.entries(data)) {
       // Iterate over each entry of the tag data and create a diff
       // with the last persisted information.
-      if (SKIP_IN_DIFF.has(key) || silent && LITERAL_TERMS.has(key)) {
+      if (
+        objectProperties[key]?.inverseOf != null ||
+        silent && key in dataProperties
+      ) {
         // The current property is one which is skipped in diffs.
         //
         // In a silent persist, this includes any literal terms.
         // The current property is one which is skipped in diffs.
         //
         // In a silent persist, this includes any literal terms.
@@ -1097,24 +540,13 @@ class Tag {
           };
         } else if (value instanceof Set) {
           // The current property is set‐valued.
           };
         } else if (value instanceof Set) {
           // The current property is set‐valued.
-          let values = null; // initialized on first use
           const oldValues = new Set(persisted);
           const newValues = new Set(value);
           for (const existing of persisted) {
             // Iterate over each persisted property and either remove
             // it from the list of new values or add it to the list of
             // removed ones.
           const oldValues = new Set(persisted);
           const newValues = new Set(value);
           for (const existing of persisted) {
             // Iterate over each persisted property and either remove
             // it from the list of new values or add it to the list of
             // removed ones.
-            //
-            // ※ Some special handling is required here for
-            // language‐tagged strings.
-            if (
-              value.has(existing) ||
-              Object(existing) === existing &&
-                (values ??= [...value]).some(($) =>
-                  `${$}` == `${existing}` &&
-                  $.language == existing.language
-                )
-            ) {
+            if (value.has(existing)) {
               // The value is in both the old and new version of the
               // data.
               oldValues.delete(existing);
               // The value is in both the old and new version of the
               // data.
               oldValues.delete(existing);
@@ -1124,10 +556,7 @@ class Tag {
               /* do nothing */
             }
           }
               /* do nothing */
             }
           }
-          diffs[key] = {
-            old: oldValues,
-            new: newValues,
-          };
+          diffs[key] = { old: oldValues, new: newValues };
         } else if (
           `${value}` != `${persisted}` ||
           value.language != persisted.language
         } else if (
           `${value}` != `${persisted}` ||
           value.language != persisted.language
@@ -1140,10 +569,7 @@ class Tag {
           };
         } else {
           // The current property did not change.
           };
         } else {
           // The current property did not change.
-          diffs[key] = {
-            old: new Set(),
-            new: new Set(),
-          };
+          diffs[key] = { old: new Set(), new: new Set() };
         }
       }
     }
         }
       }
     }
@@ -1159,38 +585,41 @@ class Tag {
     }
     const persistedIdentifier = this.#identifier;
     this.#persistedData = tagData(data); // cloning here is necessary
     }
     const persistedIdentifier = this.#identifier;
     this.#persistedData = tagData(data); // cloning here is necessary
-    for (
-      const [term, inverse] of [
-        ["broader", "narrower"],
-        ["inCanon", "hasInCanon"],
-        ["involves", "involvedIn"],
-      ]
-    ) {
-      // Iterate over each term referencing other tags and update the
-      // inverse property on those tags if possible.
-      for (const referencedIdentifier of diffs[term].old) {
-        // Iterate over the removed tags and remove this `Tag` from
-        // their inverse property.
-        const referenced = storage.get(referencedIdentifier);
-        try {
-          // Try removing this `Tag`.
-          referenced.#data[inverse].delete(persistedIdentifier);
-          storage.set(referencedIdentifier, referenced);
-        } catch {
-          // Removal failed, possibly because the other tag was
-          // deleted.
-          /* do nothing */
+    for (const inverse in objectProperties) {
+      // Iterate over each non‐transitive inverse property and update
+      // it based on its inverse on the corresponding tags if possible.
+      const term = objectProperties[inverse].inverseOf;
+      if (term == null || term in transitiveProperties) {
+        // The current property is not the inverse of an non‐transitive
+        // property.
+        /* do nothing */
+      } else {
+        // The current property is the inverse of a non‐transitive
+        // property.
+        for (const referencedIdentifier of diffs[term].old) {
+          // Iterate over the removed tags and remove this `Tag` from
+          // their inverse property.
+          const referenced = storage.get(referencedIdentifier);
+          try {
+            // Try removing this `Tag`.
+            referenced.#data[inverse].delete(persistedIdentifier);
+            storage.set(referencedIdentifier, referenced);
+          } catch {
+            // Removal failed, possibly because the other tag was
+            // deleted.
+            /* do nothing */
+          }
         }
         }
-      }
-      for (const referencedIdentifier of diffs[term].new) {
-        const referenced = storage.get(referencedIdentifier);
-        try {
-          // Try adding this `Tag`.
-          referenced.#data[inverse].add(persistedIdentifier);
-          storage.set(referencedIdentifier, referenced);
-        } catch {
-          // Adding failed, possibly because the other tag was deleted.
-          /* do nothing */
+        for (const referencedIdentifier of diffs[term].new) {
+          const referenced = storage.get(referencedIdentifier);
+          try {
+            // Try adding this `Tag`.
+            referenced.#data[inverse].add(persistedIdentifier);
+            storage.set(referencedIdentifier, referenced);
+          } catch {
+            // Adding failed, possibly because the other tag was deleted.
+            /* do nothing */
+          }
         }
       }
     }
         }
       }
     }
@@ -1232,14 +661,11 @@ class Tag {
             // things as needed.
             for (const oldValue of oldValues) {
               // Iterate over removals and unstate them.
             // things as needed.
             for (const oldValue of oldValues) {
               // Iterate over removals and unstate them.
-              if (LITERAL_TERMS.has(term)) {
-                // This is a literal term; push the change wrapped in an
-                // object.
+              if (term in dataProperties) {
+                // This is a literal term; push it.
                 unstates.push({
                   predicate: term,
                 unstates.push({
                   predicate: term,
-                  object: Object(oldValue) === oldValue
-                    ? { ...langString(oldValue) }
-                    : { "@value": `${oldValue}` },
+                  object: { ...oldValue },
                 });
               } else {
                 // This is a named term; attempt to get its I·R·I and
                 });
               } else {
                 // This is a named term; attempt to get its I·R·I and
@@ -1266,14 +692,11 @@ class Tag {
             }
             for (const newValue of newValues) {
               // Iterate over additions and state them.
             }
             for (const newValue of newValues) {
               // Iterate over additions and state them.
-              if (LITERAL_TERMS.has(term)) {
-                // This is a literal term; push the change wrapped in an
-                // object.
+              if (term in dataProperties) {
+                // This is a literal term; push it.
                 states.push({
                   predicate: term,
                 states.push({
                   predicate: term,
-                  object: Object(newValue) === newValue
-                    ? { ...langString(newValue) }
-                    : { "@value": `${newValue}` },
+                  object: { ...newValue },
                 });
               } else {
                 // This is a named term; attempt to get its I·R·I and
                 });
               } else {
                 // This is a named term; attempt to get its I·R·I and
@@ -1372,6 +795,443 @@ class Tag {
   }
 }
 
   }
 }
 
+const {
+  /**
+   * A `Tag` constructor function.
+   *
+   * This class extends the identity function, meaning that the object
+   * provided as the constructor is used verbatim (with new private
+   * fields added).
+   *
+   * ※ The instance methods of this class are provided as static
+   * methods on the superclass which all `Tag` constructors inherit
+   * from.
+   *
+   * ※ This class is not exposed.
+   */
+  TagConstructor,
+
+  /**
+   * The exposed constructor function from which all `Tag` constructors
+   * inherit.
+   *
+   * ☡ This constructor always throws.
+   */
+  TagSuper,
+} = (() => {
+  const tagConstructorBehaviours = Object.create(null);
+  return {
+    TagConstructor: class extends identity {
+      /**
+       * The `TagSystem` used for `Tag`s constructed by this
+       * constructor.
+       */
+      #system;
+
+      /** The `Storage` managed by this constructor’s `TagSystem`. */
+      #storage;
+
+      /** The schema in use for this constructor. */
+      #schema;
+
+      /**
+       * Constructs a new `Tag` constructor by adding the appropriate
+       * private fields to the provided constructor, setting its
+       * prototype, and then returning it.
+       *
+       * ※ This constructor does not modify the `name` or `prototype`
+       * properties of the provided constructor.
+       *
+       * ※ See `Tag.For`, where this constructor is used.
+       */
+      constructor(constructor, system, storage, schema) {
+        super(constructor);
+        Object.setPrototypeOf(this, TagSuper);
+        this.#system = system;
+        this.#storage = storage;
+        this.#schema = schema;
+      }
+
+      static {
+        // Define the superclass constructor which all `Tag`
+        // constructors will inherit from.
+        const superclass = tagConstructorBehaviours.TagSuper =
+          function Tag() {
+            throw new TypeError("Tags must belong to a System.");
+          };
+        const { prototype: methods } = this;
+        delete methods.constructor;
+        Object.defineProperty(superclass, "prototype", {
+          configurable: false,
+          enumerable: false,
+          value: Tag.prototype,
+          writable: false,
+        });
+        Object.defineProperties(
+          superclass,
+          Object.getOwnPropertyDescriptors(methods),
+        );
+      }
+
+      /**
+       * Yields the tags in the `TagSystem` associated with this
+       * constructor.
+       */
+      *all() {
+        const system = this.#system;
+        const storage = this.#storage;
+        for (const instance of storage.values()) {
+          // Iterate over the entries and yield the ones which are
+          // `Tag`s in this `TagSystem`.
+          if (Tag.getSystem(instance) == system) {
+            // The current instance is a `Tag` in this `TagSystem`.
+            yield instance;
+          } else {
+            // The current instance is not a `Tag` in this
+            // `TagSystem`.
+            /* do nothing */
+          }
+        }
+      }
+
+      /**
+       * Returns a new `Tag` resolved from the provided I·R·I.
+       *
+       * ☡ This function throws if the I·R·I is not in the `.iriSpace`
+       * of the `TagSystem` associated with this constructor.
+       *
+       * ※ If the I·R·I is not recognized, this function returns
+       * `null`.
+       */
+      fromIRI(iri) {
+        const system = this.#system;
+        const storage = this.#storage;
+        const name = `${iri}`;
+        const prefix = `${system.iriSpace}`;
+        if (!name.startsWith(prefix)) {
+          // The I·R·I does not begin with the expected prefix.
+          throw new RangeError(
+            `I·R·I did not begin with the expected prefix: ${iri}`,
+          );
+        } else {
+          // The I·R·I begins with the expected prefix.
+          const identifier = name.substring(prefix.length);
+          try {
+            // Attempt to resolve the identifier.
+            const instance = storage.get(identifier);
+            return Tag.getSystem(instance) == system ? instance : null;
+          } catch {
+            // Do not throw for bad identifiers.
+            return null;
+          }
+        }
+      }
+
+      /**
+       * Returns a new `Tag` resolved from the provided identifier.
+       *
+       * ☡ This function throws if the identifier is invalid.
+       *
+       * ※ If the identifier is valid but not recognized, this
+       * function returns `null`.
+       */
+      fromIdentifier(identifier) {
+        const system = this.#system;
+        const storage = this.#storage;
+        const instance = storage.get(identifier);
+        return Tag.getSystem(instance) == system ? instance : null;
+      }
+
+      /**
+       * Returns a new `Tag` resolved from the provided Tag U·R·I.
+       *
+       * ☡ This function throws if the provided Tag U·R·I does not
+       * match the tagging entity of this constructor’s `TagSystem`.
+       *
+       * ※ If the specific component of the Tag U·R·I is not
+       * recognized, this function returns `null`.
+       */
+      fromTagURI(tagURI) {
+        const system = this.#system;
+        const storage = this.#storage;
+        const tagName = `${tagURI}`;
+        const tagPrefix = `tag:${system.taggingEntity}:`;
+        if (!tagName.startsWith(tagPrefix)) {
+          // The Tag U·R·I does not begin with the expected prefix.
+          throw new RangeError(
+            `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
+          );
+        } else {
+          // The I·R·I begins with the expected prefix.
+          const identifier = tagName.substring(tagPrefix.length);
+          try {
+            // Attempt to resolve the identifier.
+            const instance = storage.get(identifier);
+            return Tag.getSystem(instance) == system ? instance : null;
+          } catch {
+            // Do not throw for bad identifiers.
+            return null;
+          }
+        }
+      }
+
+      /**
+       * Yields the tag identifiers in the `TagSystem` associated with
+       * this constructor.
+       */
+      *identifiers() {
+        const system = this.#system;
+        const storage = this.#storage;
+        for (const [identifier, instance] of storage.entries()) {
+          // Iterate over the entries and yield the ones which are
+          // `Tag`s in this `TagSystem`.
+          if (Tag.getSystem(instance) == system) {
+            // The current instance is a `Tag` in this `TagSystem`.
+            yield identifier;
+          } else {
+            // The current instance is not a `Tag` in this `TagSystem`.
+            /* do nothing */
+          }
+        }
+      }
+
+      /**
+       * Returns a new `Tag` constructed from the provided data and
+       * with the provided identifier.
+       *
+       * ※ This function is not really intended for public usage.
+       */
+      [Storage.toInstance](data, identifier) {
+        const tag = new this(data.kind);
+        return Tag.assignData(tag, data, identifier);
+      }
+    },
+    TagSuper: tagConstructorBehaviours.TagSuper,
+  };
+})();
+
+const {
+  /**
+   * Returns whether the provided schema, subject class, object
+   * property, and object class are consistent.
+   *
+   * This is hardly a full reasoner; it is tuned to the abilites and
+   * needs of this module.
+   */
+  isObjectPredicateOK,
+} = (() => {
+  const cachedClassAndSuperclasses = new WeakMap();
+  const cachedClassRestrictions = new WeakMap();
+  const cachedPredicateRestrictions = new WeakMap();
+
+  const classAndSuperclasses = function* (
+    classes,
+    baseClass,
+    touched = new Set(),
+  ) {
+    if (baseClass == "Thing" || touched.has(baseClass)) {
+      /* do nothing */
+    } else {
+      yield baseClass;
+      touched.add(baseClass);
+      const subClassOf = classes[baseClass]?.subClassOf ?? "Thing";
+      for (
+        const superclass of (
+          typeof subClassOf == "string"
+            ? [subClassOf]
+            : Array.from(subClassOf)
+        ).filter(($) => typeof $ == "string")
+      ) {
+        yield* classAndSuperclasses(classes, superclass, touched);
+      }
+    }
+  };
+
+  const getClassAndSuperclasses = (schema, baseClass) => {
+    const schemaCache = cachedClassAndSuperclasses.get(schema);
+    const cached = schemaCache?.[baseClass];
+    if (cached != null) {
+      return cached;
+    } else {
+      const { classes } = schema;
+      const result = [...classAndSuperclasses(classes, baseClass)];
+      if (schemaCache) {
+        schemaCache[baseClass] = result;
+      } else {
+        cachedClassRestrictions.set(
+          schema,
+          Object.assign(Object.create(null), { [baseClass]: result }),
+        );
+      }
+      return result;
+    }
+  };
+
+  const getClassRestrictions = (schema, domain) => {
+    const schemaCache = cachedClassRestrictions.get(schema);
+    const cached = schemaCache?.[domain];
+    if (cached != null) {
+      return cached;
+    } else {
+      const { classes } = schema;
+      const restrictions = Object.create(null);
+      const subClassOf = classes[domain]?.subClassOf ?? "Thing";
+      for (
+        const superclass of (
+          typeof subClassOf == "string"
+            ? [subClassOf]
+            : Array.from(subClassOf)
+        ).filter(($) => Object($) === $)
+      ) {
+        const { onProperty, allValuesFrom } = superclass;
+        restrictions[onProperty] = processSpace(allValuesFrom);
+      }
+      if (schemaCache) {
+        schemaCache[domain] = restrictions;
+      } else {
+        cachedClassRestrictions.set(
+          schema,
+          Object.assign(Object.create(null), {
+            [domain]: restrictions,
+          }),
+        );
+      }
+      return restrictions;
+    }
+  };
+
+  const getPredicateRestrictions = (schema, predicate) => {
+    const schemaCache = cachedPredicateRestrictions.get(schema);
+    const cached = schemaCache?.[predicate];
+    if (cached != null) {
+      return cached;
+    } else {
+      const { objectProperties } = schema;
+      const restrictions = [
+        ...predicateRestrictions(objectProperties, predicate),
+      ].reduce(
+        (result, { domainIntersection, rangeIntersection }) => {
+          result.domainIntersection.push(...domainIntersection);
+          result.rangeIntersection.push(...rangeIntersection);
+          return result;
+        },
+        Object.assign(Object.create(null), {
+          domainIntersection: [],
+          rangeIntersection: [],
+        }),
+      );
+      if (schemaCache) {
+        schemaCache[predicate] = restrictions;
+      } else {
+        cachedPredicateRestrictions.set(
+          schema,
+          Object.assign(Object.create(null), {
+            [predicate]: restrictions,
+          }),
+        );
+      }
+      return restrictions;
+    }
+  };
+
+  const processSpace = (space) =>
+    Object(space) === space
+      ? "length" in space
+        ? Array.from(
+          space,
+          (subspace) =>
+            Object(subspace) === subspace
+              ? Array.from(subspace.unionOf)
+              : [subspace],
+        )
+        : [Array.from(space.unionOf)]
+      : [[space]];
+
+  const predicateRestrictions = function* (
+    objectProperties,
+    predicate,
+    touched = new Set(),
+  ) {
+    if (predicate == "Property" || touched.has(predicate)) {
+      /* do nothing */
+    } else {
+      const { domain, range, subPropertyOf } =
+        objectProperties[predicate];
+      yield Object.assign(Object.create(null), {
+        domainIntersection: processSpace(domain ?? "Thing"),
+        rangeIntersection: processSpace(range ?? "Thing"),
+      });
+      touched.add(predicate);
+      for (
+        const superproperty of (
+          subPropertyOf == null
+            ? ["Property"]
+            : typeof subPropertyOf == "string"
+            ? [subPropertyOf]
+            : Array.from(subPropertyOf)
+        )
+      ) {
+        yield* predicateRestrictions(
+          objectProperties,
+          superproperty,
+          touched,
+        );
+      }
+    }
+  };
+
+  return {
+    isObjectPredicateOK: (
+      schema,
+      subjectClass,
+      predicate,
+      objectClass,
+    ) => {
+      const { objectProperties } = schema;
+      const predicateDefinition = objectProperties[predicate];
+      const isInverse = "inverseOf" in predicateDefinition;
+      const usedPredicate = isInverse
+        ? predicateDefinition.inverseOf
+        : predicate;
+      const domain = isInverse ? objectClass : subjectClass;
+      const domains = new Set(getClassAndSuperclasses(schema, domain));
+      const ranges = new Set(getClassAndSuperclasses(
+        schema,
+        isInverse ? subjectClass : objectClass,
+      ));
+      const predicateRestrictions = getPredicateRestrictions(
+        schema,
+        usedPredicate,
+      );
+      const { domainIntersection } = predicateRestrictions;
+      const rangeIntersection = [
+        ...predicateRestrictions.rangeIntersection,
+        ...function* () {
+          for (const domain of domains) {
+            const classRestrictionOnPredicate =
+              getClassRestrictions(schema, domain)[usedPredicate];
+            if (classRestrictionOnPredicate != null) {
+              yield* classRestrictionOnPredicate;
+            } else {
+              /* do nothing */
+            }
+          }
+        }(),
+      ];
+      return domainIntersection.every((domainUnion) =>
+        domainUnion.some((domain) =>
+          domain == "Thing" || domains.has(domain)
+        )
+      ) &&
+        rangeIntersection.every((rangeUnion) =>
+          rangeUnion.some((range) =>
+            range == "Thing" || ranges.has(range)
+          )
+        );
+    },
+  };
+})();
+
 const {
   /**
    * Returns the provided value converted into a `String` object with
 const {
   /**
    * Returns the provided value converted into a `String` object with
@@ -1669,47 +1529,7 @@ export class TagSystem {
     } else {
       // No bound constructor has been created yet.
       const storage = this.#storage;
     } else {
       // No bound constructor has been created yet.
       const storage = this.#storage;
-      const BoundTag = Tag.bind(undefined, this, storage);
-      return this.#Tag = Object.defineProperties(BoundTag, {
-        all: {
-          configurable: true,
-          enumerable: false,
-          value: Tag.all.bind(BoundTag, this, storage),
-          writable: true,
-        },
-        fromIRI: {
-          configurable: true,
-          enumerable: false,
-          value: Tag.fromIRI.bind(BoundTag, this, storage),
-          writable: true,
-        },
-        fromIdentifier: {
-          configurable: true,
-          enumerable: false,
-          value: Tag.fromIdentifier.bind(BoundTag, this, storage),
-          writable: true,
-        },
-        fromTagURI: {
-          configurable: true,
-          enumerable: false,
-          value: Tag.fromTagURI.bind(BoundTag, this, storage),
-          writable: true,
-        },
-        identifiers: {
-          configurable: true,
-          enumerable: false,
-          value: Tag.identifiers.bind(BoundTag, this, storage),
-          writable: true,
-        },
-        name: { value: `${this.tagURI}#${Tag.name}` },
-        prototype: { value: Tag.prototype },
-        [Storage.toInstance]: {
-          configurable: true,
-          enumerable: false,
-          value: Tag[Storage.toInstance].bind(BoundTag, this, storage),
-          writable: true,
-        },
-      });
+      return this.#Tag = Tag.For(this, storage, schema);
     }
   }
 
     }
   }
 
This page took 0.161889 seconds and 4 git commands to generate.