Repository: OpenAssetIO/OpenAssetIO (language: python)

Issue:
Remove contraint on unique trait sets in `register`
## What

Remove the assertion in `register` that the trait sets of the supplied `TraitsData` instances should be the same.

## Why

See #1015. 

## Acceptance Criteria

- `register` can be used with divergent trait sets in the supplied `TraitsData` instances.
- A manager can determine if the `TraitsData` instances supplied to the `register` method share a common trait set with a single line of code.
- Docs that describe publishing updated as needed.

## Notes

- No need for `apiComplianceSuite` check (we don't validate batch variance for other methods)

## Tasks

- [x] #1037
Prefix glossary refs in Doxygen comments
# What

Prefix the cross-reference labels of glossary page items with some token and adjust the label text case to match the glossary title, e.g. `attributes` -> `gloss:Attributes`.

# Why

* When reading the documentation in the source it would be useful to know at a glance where a ref points to, in particular whether it points to the glossary or some class/method/variable.
* When writing the documentation it would be useful to have the case of the ref match the case of the rendered output, to help visualise how the paragraph will look.

Preflight `TraitsData` and relax `register` trait set homogeneity
## Description

Closes #1028
Closes #1029

Changes the signature of `preflight` to accept a `TraitsData` per reference, rather than a single trait set applied to all references. Existing docs and examples updated.

Simultaneously relaxes the restriction that all batch published entities must be of the same type.

- [x] I have updated the release notes.
- [x] I have updated all relevant user documentation.

## Reviewer Notes

Combined two issues into one PR here, since it avoids adding an artificial restriction to `preflight` to ensure trait set homogeneity, just to remove it again in the next issue.

Commits split into roughly related chunks of work, intended to be squashed into a single commit once approved.

Minimal changes to BAL https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/pull/63 to get these API changes in as soon as possible.

Prefix glossary refs in Doxygen comments
# What

Prefix the cross-reference labels of glossary page items with some token and adjust the label text case to match the glossary title, e.g. `attributes` -> `gloss:Attributes`.

# Why

* When reading the documentation in the source it would be useful to know at a glance where a ref points to, in particular whether it points to the glossary or some class/method/variable.
* When writing the documentation it would be useful to have the case of the ref match the case of the rendered output, to help visualise how the paragraph will look.



Discussion / hints:

Just encountered an issue where I wanted to `@ref resolve` from within `ManagerInterface`/`Manager`, expecting it to link to the member function, when it actually links to the Glossary. I notice we have some similar cases in those classes. Tackling this issue would fix the problem automatically.

Just encountered an issue where I wanted to `@ref resolve` from within `ManagerInterface`/`Manager`, expecting it to link to the member function, when it actually links to the Glossary. I notice we have some similar cases in those classes. Tackling this issue would fix the problem automatically.

Fix patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 3dfdb6e67..1d5286bd4 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -6,6 +6,17 @@ v1.0.0-alpha.x
 
 ### Breaking changes
 
+- Changed signature of `preflight` to accept a `TraitsData` per
+  reference, rather than a single trait set. The host is now expected to
+  communicate any relevant information that it owns and is known at
+  `preflight` time.
+  [1028](https://github.com/OpenAssetIO/OpenAssetIO/issues/1028)
+
+- Relaxed the restriction on `register` that all trait sets of the
+  provided `TraitsData` elements must match. This allows batched
+  publishing of heterogeneous entity types.
+  [1029](https://github.com/OpenAssetIO/OpenAssetIO/issues/1029)
+
 - Removed `Context.Access.kWriteMultiple` and
  `Context.Access.kReadMultiple`access patterns, due to not having
  coherent use cases.
diff --git a/doc/doxygen/src/Configuration.dox b/doc/doxygen/src/Configuration.dox
index d0dd8981e..d5bd95195 100644
--- a/doc/doxygen/src/Configuration.dox
+++ b/doc/doxygen/src/Configuration.dox
@@ -27,8 +27,8 @@
  *
  * This page covers hows to configure OpenAssetIO so that the hosts you
  * use (e.g. @ref DCC "DCC tools", applications and other scripts) use a
- * specific manager of your choice to @ref resolve and potentially @ref
- * publish data.
+ * specific manager of your choice to @ref glossary_resolve and
+ * potentially @ref publish data.
  *
  * OpenAssetIO itself is usually built into any host that supports it,
  * this means you don't normally need do to anything to ensure the API
diff --git a/doc/doxygen/src/EntitiesTraitsSpecifications.dox b/doc/doxygen/src/EntitiesTraitsSpecifications.dox
index 2fa6ee1f7..c0c529081 100644
--- a/doc/doxygen/src/EntitiesTraitsSpecifications.dox
+++ b/doc/doxygen/src/EntitiesTraitsSpecifications.dox
@@ -213,8 +213,8 @@
  *
  * - The context @ref locale - to describe the specifics of the
  *   host environment calling the API.
- * - During @ref register and @ref resolve - to hold the data for an
- *   @ref entity.
+ * - During @ref glossary_register and @ref glossary_resolve - to hold
+ *   the data for an @ref entity.
  * - Describing Relationships - to define some connection between one or
  *   more entities.
  *
@@ -238,10 +238,10 @@
  *
  * @subsection entity_specifications Entity Specifications
  *
- * Specifictions used with @ref register and returned by @ref resolve to
- * hold @ref entity data are known as "entity specifications". The
- * @ref trait_set they define can also be used as a shorthand for
- * consistency when:
+ * Specifictions used with @ref glossary_register and returned by @ref
+ * glossary_resolve to hold @ref entity data are known as "entity
+ * specifications". The @ref trait_set they define can also be used as a
+ * shorthand for consistency when:
  *
  *  - A host wishes to know if a manager would like to be involved in
  *    reads or writes of a specific kind of data.
@@ -315,7 +315,7 @@
  * @subsection Publishing
  *
  * When populating specifications with traits for publishing (see @ref
- * register). The relevant traits are the ones that cover the
+ * glossary_register). The relevant traits are the ones that cover the
  * data that is registered, so that that data is then available later
  * on for `resolve` or when evaluating filter predicates.
  *
diff --git a/doc/doxygen/src/Examples.dox b/doc/doxygen/src/Examples.dox
index 563ec8ff0..d5c321fec 100644
--- a/doc/doxygen/src/Examples.dox
+++ b/doc/doxygen/src/Examples.dox
@@ -277,9 +277,13 @@
  *
  * # Whenever we make new data, we always tell the manager first,
  * # This allows it to create a placeholder version or similar.
+ * # We must provide the manager with any relevant information that the
+ * # host owns (i.e. won't be queried from the manager during
+ * # publishing) and can be provided up-front.
+ * file_spec = TextFileSpecification.create()
+ * file_spec.markupTrait().setMarkupType("plain")
  * # NOTE: It is critical to always use the working_ref from now on.
- * working_ref = manager.preflight(
- *         entity_ref, TextFileSpecification.kTraitSet, context)
+ * working_ref = manager.preflight(entity_ref, file_spec, context)
  *
  * # We then check if the manager can tell us where to save the file.
  * if ResolvesFutureEntitiesTrait.isImbuedTo(policy):
@@ -297,7 +301,6 @@
  *
  * # Prepare the entity specification to register, with the data about
  * # where we actually wrote the data to, and with what encoding.
- * file_spec = TextFileSpecification.create()
  * file_spec.locatableContentTrait().setLocation(pathToURL(save_path))
  * file_spec.textEncodingTrait().setEncoding(encoding)
  *
diff --git a/doc/doxygen/src/ForHosts.dox b/doc/doxygen/src/ForHosts.dox
index d3ebd211d..f1523d4a9 100644
--- a/doc/doxygen/src/ForHosts.dox
+++ b/doc/doxygen/src/ForHosts.dox
@@ -42,7 +42,7 @@
  *   "Manager" require an appropriately configured @ref Context. This
  *   tells the @ref asset_management_system about the intended actions
  *   of the host. For example, whether an @ref entity_reference is being
- *   @ref resolve "resolved" for read or for write.
+ *   @ref glossary_resolve "resolved" for read or for write.
  *
  * - The lifetime of the Context can be carefully managed by the host
  *   to allow the manager to correlate and time-lock disparate API calls.
@@ -133,7 +133,8 @@
  *   managementPolicy response.
  *
  * - Follow the @fqref{hostApi.Manager.managementPolicy} "policy", @ref
- *   preflight, resolve, write, @ref register process illustrated @ref
+ *   glossary_preflight, @ref glossary_resolve, write, @ref
+ *   glossary_register process illustrated @ref
  *   example_publishing_a_file "here" whenever generating new data.
  *
  * - Include in your documentation, any scenarios in which entities are
diff --git a/doc/doxygen/src/ForManagers.dox b/doc/doxygen/src/ForManagers.dox
index 8180a3ebe..b00a79166 100644
--- a/doc/doxygen/src/ForManagers.dox
+++ b/doc/doxygen/src/ForManagers.dox
@@ -60,7 +60,7 @@
  *   intentions and/or requirements of the caller, as well as to
  *   determine which part of an application is involved in the call.
  *   This can be used to help determine the correct values for @ref
- *   trait properties during @ref resolve.
+ *   trait properties during @ref glossary_resolve.
  *
  * - Many API calls are passed a @ref trait_set. They form a strong type
  *   mechanism, and must be respected as a filter predicate for
diff --git a/doc/doxygen/src/Glossary.dox b/doc/doxygen/src/Glossary.dox
index f7cd6e48c..9ab8e48b6 100644
--- a/doc/doxygen/src/Glossary.dox
+++ b/doc/doxygen/src/Glossary.dox
@@ -366,7 +366,7 @@
  * indirection allows sundry house keeping and state management.
  *
  *
- * @section preflight preflight
+ * @section glossary_preflight preflight
  *
  * @ref asset_management_system "Asset management systems" are roughly
  * divided into two camps - those that can tell a @ref host in advance
@@ -381,6 +381,38 @@
  * returned @ref entity_reference to determine the manager's chosen
  * location for the data and use this for any write operations.
  *
+ * A host should always @ref publish the full set of @ref trait "traits"
+ * of an appropriate @ref Specification "specification" for the type of
+ * entity being published.
+ *
+ * Some of a specification's traits may hold properties, and some of
+ * those properties may be required by
+ * @fqref{managerApi.ManagerInterface.preflight} "preflight" in order
+ * for subsequent operations in the publishing workflow to succeed.
+ * Other properties may be optional.
+ *
+ * The host should fill in and pass as a "hint" to
+ * @fqref{hostApi.Manager.preflight} "preflight"  any trait properties
+ * that are owned by the host and known at the time `preflight` is
+ * called.
+ *
+ * In this context, a property is "owned" by the host if it will not be
+ * dictated by the manager during the publishing workflow (i.e. by @ref
+ * glossary_resolve "resolving" with a @fqref{Context.Access.kWrite}
+ * "write" access flag). Note that this definition of host ownership
+ * might include properties that have been resolved from the manager at
+ * some point in the past.
+ *
+ * If a property is not already known or understood by the host, and is
+ * optional, then it may be omitted.
+ *
+ * Providing the preflight hint allows the manager to prepare the
+ * backend and provide the most appropriate responses to subsequent @ref
+ * glossary_resolve and @ref glossary_register requests; or
+ * @fqref{BatchElementError.ErrorCode.kInvalidPreflightHint} "error"
+ * early if publishing is likely to fail or insufficient information is
+ * given in the hint.
+ *
  * The `preflight` method is also allowed to change the entity
  * reference as required. This mechanism serves several purposes:
  *
@@ -401,9 +433,9 @@
  * - When the data already exists at some target location, such as when
  *   assetizing an existing file.
  *
- * In these situations, only @ref register will be called by the host.
- * In the first situation, calling `preflight` would be pointless
- * as the manager has no way to provide a meaningful answer.
+ * In these situations, only @ref glossary_register will be called by
+ * the host. In the first situation, calling `preflight` would be
+ * pointless as the manager has no way to provide a meaningful answer.
  *
  * In the second situation it removes the responsibility for the host to
  * transfer the data from its current location to that specified by
@@ -433,13 +465,13 @@
  *   @ref entity "entity's" @ref trait "traits", via the manager's
  *   @fqref{hostApi.Manager.managementPolicy} "managementPolicy", and
  *   what traits it is capable of persisting property data for.
- * - Call @ref preflight to allow any prerequisite housekeeping
+ * - Call @ref glossary_preflight to allow any prerequisite housekeeping
  *   to be performed, and a working @ref entity_reference returned.
  * - Resolve the working reference to determine the URL for new data to
  *   be written to.
  * - Write the data accordingly.
- * - Call @ref register to finalize the publish and confirm to the
- *   manager that the data has been written.
+ * - Call @ref glossary_register to finalize the publish and confirm to
+ *   the manager that the data has been written.
  *
  * @note Some managers may not be able to determine trait properties in
  * advance, and act more as librarians. This is indicated in the
@@ -447,7 +479,7 @@
  *
  * When publishing existing data, the preflight step is omitted.
  *
- * @section register register
+ * @section glossary_register register
  *
  * The registration mechanism allows for a @ref host to create or update
  * an @ref entity. This is the sole mechanism by which data can be
@@ -472,7 +504,7 @@
  * @fqref{Context.Access.kCreateRelated} "Context.Access.kCreateRelated"
  * in the context is used as a flag to indicate this intent.
  *
- * @section resolve resolve
+ * @section glossary_resolve resolve
  *
  * 'Resolving' an @ref entity_reference is the process of turning it
  * back into the data for one or more @ref trait "traits". Resolution is
@@ -513,9 +545,9 @@
  * A trait may optionally define one or more properties that hold
  * simple-typed values.
  *
- * When an entity is @ref publish "published" or @ref resolve "resolved"
- * through the API, these trait properties are used to hold the entity's
- * data.
+ * When an entity is @ref publish "published" or @ref glossary_resolve
+ * "resolved" through the API, these trait properties are used to hold
+ * the entity's data.
  *
  * @see @ref entities_traits_and_specifications
  *
diff --git a/doc/doxygen/src/MainPage.dox b/doc/doxygen/src/MainPage.dox
index f44caf3fb..a0087699a 100644
--- a/doc/doxygen/src/MainPage.dox
+++ b/doc/doxygen/src/MainPage.dox
@@ -41,8 +41,8 @@
  * @section intro_scope Scope
  *
  * The API covers the following areas:
- * - @ref resolve "Resolution" of asset references (URIs) into a
- *   dictionary of data, grouped into one or more @ref trait "traits"
+ * - @ref glossary_resolve "Resolution" of asset references (URIs) into
+ *   a dictionary of data, grouped into one or more @ref trait "traits"
  *   (providing URLs for access and other asset data).
  * - @ref publish "Publishing" data for file-based and non-file-based
  *   assets.
diff --git a/doc/doxygen/src/Thumbnails.dox b/doc/doxygen/src/Thumbnails.dox
index 74a4109a8..edbbb4d85 100644
--- a/doc/doxygen/src/Thumbnails.dox
+++ b/doc/doxygen/src/Thumbnails.dox
@@ -29,13 +29,14 @@
  * "managementPolicy" for any given set of entity @ref trait "traits".
  *
  * If supported by the host, it will then:
- * - Call @ref preflight with the target entity and the traits from the
- *   @needsref ThumbnailSpecification, using the
-     @fqref{Context.Access.kCreateRelated} "Access.kCreateRelated"
-     access mode.
- * - Call @needsref resolve on the returned reference to determine the
- *   desired width/height/format for the thumbnail.
- * - Generate a thumbnail and @ref register it to the same reference.
+ * - Call @ref glossary_preflight with the target entity and the traits
+ *   data from the @needsref ThumbnailSpecification, using the
+ *   @fqref{Context.Access.kCreateRelated} "Access.kCreateRelated"
+ *   access mode.
+ * - Call @ref glossary_resolve on the returned reference to determine
+ *   the desired width/height/format for the thumbnail.
+ * - Generate a thumbnail and @ref glossary_register it to the same
+ *   reference.
  *
  * @note The thumbnail generation process may be carried out
  * asynchronously by the host and registration may occur at an arbitrary
diff --git a/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.cpp b/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.cpp
index ad0b7b843..e20b4ce7d 100644
--- a/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.cpp
+++ b/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.cpp
@@ -110,7 +110,7 @@ void CManagerInterfaceAdapter::resolve(
 
 void CManagerInterfaceAdapter::preflight(
     [[maybe_unused]] const EntityReferences& entityReferences,
-    [[maybe_unused]] const trait::TraitSet& traitSet,
+    [[maybe_unused]] const trait::TraitsDatas& traitsDatas,
     [[maybe_unused]] const ContextConstPtr& context,
     [[maybe_unused]] const HostSessionPtr& hostSession,
     [[maybe_unused]] const PreflightSuccessCallback& successCallback,
diff --git a/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.hpp b/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.hpp
index 16dafec59..6d535aff9 100644
--- a/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.hpp
+++ b/src/openassetio-core-c/src/managerApi/CManagerInterfaceAdapter.hpp
@@ -69,7 +69,7 @@ class OPENASSETIO_CORE_C_EXPORT CManagerInterfaceAdapter : ManagerInterface {
 
   /// Wrap the C suite's `preflight` function.
   /// @todo Implement C API. Currently throws `runtime_error`.
-  void preflight(const EntityReferences& entityReferences, const trait::TraitSet& traitSet,
+  void preflight(const EntityReferences& entityReferences, const trait::TraitsDatas& traitsDatas,
                  const ContextConstPtr& context, const HostSessionPtr& hostSession,
                  const PreflightSuccessCallback& successCallback,
                  const BatchElementErrorCallback& errorCallback) override;
diff --git a/src/openassetio-core/include/openassetio/BatchElementError.hpp b/src/openassetio-core/include/openassetio/BatchElementError.hpp
index 3d8a1a36a..841196080 100644
--- a/src/openassetio-core/include/openassetio/BatchElementError.hpp
+++ b/src/openassetio-core/include/openassetio/BatchElementError.hpp
@@ -84,16 +84,16 @@ class BatchElementError final {
      * Error code used when the reference is valid, but the supplied
      * @ref Context access is invalid for the operation. A common
      * example of this would be resolving a read-only entity with a
-     * write access Context, or during @ref preflight or @ref register
-     * when the target entity s read-only and does not support
-     * updating.
+     * write access Context, or during @ref glossary_preflight or @ref
+     * glossary_register when the target entity s read-only and does not
+     * support updating.
      */
     kEntityAccessError = OPENASSETIO_BatchErrorCode_kEntityAccessError,
 
     /**
-     * Error code used during @ref resolve "entity resolution" when the
-     * reference itself is valid, but it is not possible to retrieve
-     * data for the referenced @ref entity.
+     * Error code used during @ref glossary_resolve "entity resolution"
+     * when the reference itself is valid, but it is not possible to
+     * retrieve data for the referenced @ref entity.
      *
      * This could be because it does not exist, or some other
      * entity-specific reason that this data cannot be resolved for a
@@ -107,7 +107,18 @@ class BatchElementError final {
      * entity-based operations on a valid @ref entity_reference that
      * fail for some reason.
      */
-    kEntityResolutionError = OPENASSETIO_BatchErrorCode_kEntityResolutionError
+    kEntityResolutionError = OPENASSETIO_BatchErrorCode_kEntityResolutionError,
+
+    /**
+     * Error code response from @ref glossary_preflight if the provided
+     * @fqref{TraitsData} "traits data" hint holds insufficient or
+     * invalid information.
+     *
+     * This will occur when the manager requires information that the
+     * host owns to be passed to `preflight`, but the host did not
+     * provide it.
+     */
+    kInvalidPreflightHint = OPENASSETIO_BatchErrorCode_kInvalidPreflightHint
   };
 
   /**
@@ -199,6 +210,14 @@ struct OPENASSETIO_CORE_EXPORT EntityAccessErrorBatchElementException : BatchEle
 struct OPENASSETIO_CORE_EXPORT EntityResolutionErrorBatchElementException : BatchElementException {
   using BatchElementException::BatchElementException;
 };
+
+/**
+ * Exception equivalent of
+ * @ref BatchElementError.ErrorCode.kInvalidPreflightHint
+ */
+struct OPENASSETIO_CORE_EXPORT InvalidPreflightHintBatchElementException : BatchElementException {
+  using BatchElementException::BatchElementException;
+};
 /**
  * @}
  */
diff --git a/src/openassetio-core/include/openassetio/errors.h b/src/openassetio-core/include/openassetio/errors.h
index b601b684d..d12c27144 100644
--- a/src/openassetio-core/include/openassetio/errors.h
+++ b/src/openassetio-core/include/openassetio/errors.h
@@ -41,3 +41,6 @@
 
 /// Entity resolution failure.
 #define OPENASSETIO_BatchErrorCode_kEntityResolutionError (OPENASSETIO_BatchErrorCode_BEGIN + 4)
+
+/// Invalid TraitsData hint given to preflight.
+#define OPENASSETIO_BatchErrorCode_kInvalidPreflightHint (OPENASSETIO_BatchErrorCode_BEGIN + 5)
diff --git a/src/openassetio-core/include/openassetio/hostApi/Manager.hpp b/src/openassetio-core/include/openassetio/hostApi/Manager.hpp
index adab9e906..c6de711a9 100644
--- a/src/openassetio-core/include/openassetio/hostApi/Manager.hpp
+++ b/src/openassetio-core/include/openassetio/hostApi/Manager.hpp
@@ -230,7 +230,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    *
    * If a requested trait is not present, then the manager will never
    * return properties for that trait in @ref resolve, or be able to
-   * persist those properties with @ref register. This allows you to
+   * persist those properties with @ref register_. This allows you to
    * know in advance if you can expect the configured manager to be able
    * to provide data you may require.
    *
@@ -1125,7 +1125,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * interaction with the Manager, and it will have returned you
    * something meaningful).
    *
-   * It should be called before register() if you are about to
+   * It should be called before register_() if you are about to
    * create media or write to files. If the file or data already
    * exists, then preflight is not needed. It will return a working
    * @ref entity_reference for each given entity, which can be
@@ -1138,9 +1138,10 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * the Manager may even use this opportunity to switch to some
    * temporary working path or some such.
    *
-   * @warning If the supplied @ref trait_set is missing traits required
-   * by the manager for any input entity reference, then that element
-   * will error.
+   * @warning If the supplied @fqref{TraitsData} "trait data" is missing
+   * traits or properties required by the manager for any input entity
+   * reference, then that element will error. See @ref
+   * glossary_preflight "glossary entry" for details.
    *
    * @note It's vital that the @ref Context is well configured here,
    * in particular the @fqref{Context.retention}
@@ -1150,13 +1151,15 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * method should *always* be used in place of the original
    * reference supplied to `preflight` for resolves prior to
    * registration, and for the final call to @ref
-   * register itself. See @ref example_publishing_a_file.
+   * register_ itself. See @ref example_publishing_a_file.
    *
    * @param entityReferences The entity references to preflight prior
    * to registration.
    *
-   * @param traitSet The @ref trait_set of the entities that are being
-   * published.
+   * @param traitsHints @ref trait_set for each entity, determining the
+   * type of entity to publish, complete with any properties the host
+   * owns and can provide at this time. See @ref glossary_preflight
+   * "glossary entry" for details.
    *
    * @param context The calling context. This is not
    * replaced with an array in order to simplify implementation.
@@ -1179,9 +1182,9 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * "ErrorCodes"). The callback will be called on the same thread
    * that initiated the call to `preflight`.
    *
-   * @see @ref register
+   * @see @ref register_
    */
-  void preflight(const EntityReferences& entityReferences, const trait::TraitSet& traitSet,
+  void preflight(const EntityReferences& entityReferences, const trait::TraitsDatas& traitsHints,
                  const ContextConstPtr& context, const PreflightSuccessCallback& successCallback,
                  const BatchElementErrorCallback& errorCallback);
 
@@ -1192,7 +1195,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    *
    * See documentation for the <!--
    * --> @ref preflight(const EntityReferences&, <!--
-   * --> const trait::TraitSet&, const ContextConstPtr&, <!--
+   * --> const trait::TraitsDatas&, const ContextConstPtr&, <!--
    * --> const PreflightSuccessCallback&, <!--
    * --> const BatchElementErrorCallback&)
    * "callback variation" for more details on preflight behaviour.
@@ -1206,8 +1209,9 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * @param entityReference The entity reference to preflight prior
    * to registration.
    *
-   * @param traitSet The @ref trait_set of the entity that is being
-   * published.
+   * @param traitsHint @ref trait_set for the entity,
+   * determining the type of entity to publish, complete with any
+   * properties that can be provided at this time.
    *
    * @param context The calling context.
    *
@@ -1218,7 +1222,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * the publishing operation
    */
   EntityReference preflight(const EntityReference& entityReference,
-                            const trait::TraitSet& traitSet, const ContextConstPtr& context,
+                            const TraitsDataPtr& traitsHint, const ContextConstPtr& context,
                             const BatchElementErrorPolicyTag::Exception& errorPolicyTag = {});
 
   /**
@@ -1228,7 +1232,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    *
    * See documentation for the <!--
    * --> @ref preflight(const EntityReferences&, <!--
-   * --> const trait::TraitSet&, const ContextConstPtr&, <!--
+   * --> const trait::TraitsDatas&, const ContextConstPtr&, <!--
    * --> const PreflightSuccessCallback&, <!--
    * --> const BatchElementErrorCallback&)
    * "callback variation" for more details on preflight behaviour.
@@ -1245,8 +1249,9 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * @param entityReference The entity reference to preflight prior
    * to registration.
    *
-   * @param traitSet The @ref trait_set of the entity that is being
-   * published.
+   * @param traitsHint @ref trait_set for the entity,
+   * determining the type of entity to publish, complete with any
+   * properties that can be provided at this time.
    *
    * @param context The calling context.
    *
@@ -1259,7 +1264,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * entity.
    */
   std::variant<BatchElementError, EntityReference> preflight(
-      const EntityReference& entityReference, const trait::TraitSet& traitSet,
+      const EntityReference& entityReference, const TraitsDataPtr& traitsHint,
       const ContextConstPtr& context, const BatchElementErrorPolicyTag::Variant& errorPolicyTag);
 
   /**
@@ -1269,7 +1274,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    *
    * See documentation for the <!--
    * --> @ref preflight(const EntityReferences&, <!--
-   * --> const trait::TraitSet&, const ContextConstPtr&, <!--
+   * --> const trait::TraitsDatas&, const ContextConstPtr&, <!--
    * --> const PreflightSuccessCallback&, <!--
    * --> const BatchElementErrorCallback&)
    * "callback variation" for more details on preflight behaviour.
@@ -1283,8 +1288,9 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * @param entityReferences The entity references to preflight prior
    * to registration.
    *
-   * @param traitSet The @ref trait_set of the entities that are being
-   * published.
+   * @param traitsHints @ref trait_set for each entity,
+   * determining the type of entity to publish, complete with any
+   * properties that can be provided at this time.
    *
    * @param context The calling context. The same calling context is
    * used for each entity reference.
@@ -1296,7 +1302,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * of the publishing operation
    */
   EntityReferences preflight(const EntityReferences& entityReferences,
-                             const trait::TraitSet& traitSet, const ContextConstPtr& context,
+                             const trait::TraitsDatas& traitsHints, const ContextConstPtr& context,
                              const BatchElementErrorPolicyTag::Exception& errorPolicyTag = {});
 
   /**
@@ -1306,7 +1312,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    *
    * See documentation for the <!--
    * --> @ref preflight(const EntityReferences&, <!--
-   * --> const trait::TraitSet&, const ContextConstPtr&, <!--
+   * --> const trait::TraitsDatas&, const ContextConstPtr&, <!--
    * --> const PreflightSuccessCallback&, <!--
    * --> const BatchElementErrorCallback& errorCallback)
    * "callback variation" for more details on preflight behaviour.
@@ -1325,8 +1331,9 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * @param entityReferences The entity references to preflight prior
    * to registration.
    *
-   * @param traitSet The @ref trait_set of the entity that is being
-   * published.
+   * @param traitsHints @ref trait_set for each entity,
+   * determining the type of entity to publish, complete with any
+   * properties that can be provided at this time.
    *
    * @param context The calling context. The same calling context is
    * used for each entity reference.
@@ -1340,7 +1347,7 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * particular entity.
    */
   std::vector<std::variant<BatchElementError, EntityReference>> preflight(
-      const EntityReferences& entityReferences, const trait::TraitSet& traitSet,
+      const EntityReferences& entityReferences, const trait::TraitsDatas& traitsHints,
       const ContextConstPtr& context, const BatchElementErrorPolicyTag::Variant& errorPolicyTag);
 
   /**
@@ -1429,9 +1436,6 @@ class OPENASSETIO_CORE_EXPORT Manager {
    * @exception std::out_of_range If @p entityReferences and
    * @p entityTraitsDatas are not lists of the same length.
    *
-   * @exception std::invalid_argument If all @p entityTraitsDatas do
-   * not share the same trait set.
-   *
    * Other exceptions may be raised for fatal runtime errors, for
    * example server communication failure.
    *
diff --git a/src/openassetio-core/include/openassetio/managerApi/ManagerInterface.hpp b/src/openassetio-core/include/openassetio/managerApi/ManagerInterface.hpp
index cde8fe859..c867e789d 100644
--- a/src/openassetio-core/include/openassetio/managerApi/ManagerInterface.hpp
+++ b/src/openassetio-core/include/openassetio/managerApi/ManagerInterface.hpp
@@ -343,7 +343,7 @@ class OPENASSETIO_CORE_EXPORT ManagerInterface {
    *
    * @note It is not _required_ that a Host calls this method before
    * invoking other API methods, and so methods such as @ref resolve or
-   * @ref register must be tolerant of being called with unsupported
+   * @ref register_ must be tolerant of being called with unsupported
    * traits (fear not, there is a simple and established failure mode
    * for this situation).
    *
@@ -1089,7 +1089,7 @@ class OPENASSETIO_CORE_EXPORT ManagerInterface {
    *
    * If this does not apply to the manager's workflow, then the
    * method can pass back the input reference once the target entity
-   *   reference has been validated.
+   * reference has been validated.
    *
    * Generally, this will be called before register() when data is not
    * already immediately available for registration, to allow
@@ -1102,17 +1102,22 @@ class OPENASSETIO_CORE_EXPORT ManagerInterface {
    * supplied references, and callbacks have been called on the same
    * thread that called `preflight`
    *
-   * @warning If the supplied @ref trait_set is missing required traits
-   * for any of the provided references (maybe they are mismatched with
-   * the target entity, or missing essential data) then error that
-   * element with an appropriate @fqref{BatchElementError.ErrorCode}.
+   * @warning If the supplied @fqref{TraitsData} "trait data" is missing
+   * required traits for any of the provided references (maybe they are
+   * mismatched with the target entity), or the populated properties are
+   * insufficient or invalid for upcoming @ref resolve for
+   * @fqref{Context.Access.kWrite} "write" requests or the eventual
+   * @ref register_, then error that element with an appropriate
+   * @fqref{BatchElementError.ErrorCode}.
    *
    * @param entityReferences An @ref entity_reference for each entity
    * that it is desired to publish the forthcoming data to. See the
    * notes in the API documentation for the specifics of this.
    *
-   * @param traitSet The @ref trait_set of the entities that are being
-   * published.
+   * @param traitsHints @ref trait_set for each entity,
+   * determining the type of entity to publish, complete with any
+   * properties the host owns and can provide at this time. See @ref
+   * glossary_preflight "glossary entry" for more information.
    *
    * @param context The calling context. This is not replaced with an
    * array in order to simplify implementation. Otherwise, transactional
@@ -1145,16 +1150,21 @@ class OPENASSETIO_CORE_EXPORT ManagerInterface {
    * references that are conceptually read-only in response to
    * @fqref{Context.Access.kWrite} "Access.kWrite", or in response to
    * @fqref{Context.Access.kCreateRelated} "Access.kCreateRelated" if
-   * creating related entities is not supported.
+   * creating related entities is not supported. A
+   * @fqref{BatchElementError.ErrorCode.kInvalidPreflightHint}
+   * "kInvalidPreflightHint" should be used for any target references
+   * who's corresponding @p traitsHints entry holds insufficient or
+   * invalid information.
    *
    * @note it is important for the implementation to pay attention to
    * @fqref{Context.retention} "Context.retention", as not all hosts
    * will support the reference changing at this point.
    *
-   * @see @ref register
+   * @see @ref register_
    */
-  virtual void preflight(const EntityReferences& entityReferences, const trait::TraitSet& traitSet,
-                         const ContextConstPtr& context, const HostSessionPtr& hostSession,
+  virtual void preflight(const EntityReferences& entityReferences,
+                         const trait::TraitsDatas& traitsHints, const ContextConstPtr& context,
+                         const HostSessionPtr& hostSession,
                          const PreflightSuccessCallback& successCallback,
                          const BatchElementErrorCallback& errorCallback) = 0;
 
diff --git a/src/openassetio-core/src/hostApi/Manager.cpp b/src/openassetio-core/src/hostApi/Manager.cpp
index e4bb023c2..3c78bc39d 100644
--- a/src/openassetio-core/src/hostApi/Manager.cpp
+++ b/src/openassetio-core/src/hostApi/Manager.cpp
@@ -318,21 +318,29 @@ void Manager::getWithRelationshipsPaged(
                                                convertingPagerSuccessCallback, errorCallback);
 }
 
-void Manager::preflight(const EntityReferences &entityReferences, const trait::TraitSet &traitSet,
-                        const ContextConstPtr &context,
+void Manager::preflight(const EntityReferences &entityReferences,
+                        const trait::TraitsDatas &traitsHints, const ContextConstPtr &context,
                         const PreflightSuccessCallback &successCallback,
                         const BatchElementErrorCallback &errorCallback) {
-  managerInterface_->preflight(entityReferences, traitSet, context, hostSession_, successCallback,
-                               errorCallback);
+  if (entityReferences.size() != traitsHints.size()) {
+    std::string message = "Parameter lists must be of the same length: ";
+    message += std::to_string(entityReferences.size());
+    message += " entity references vs. ";
+    message += std::to_string(traitsHints.size());
+    message += " traits hints.";
+    throw std::out_of_range{message};
+  }
+  managerInterface_->preflight(entityReferences, traitsHints, context, hostSession_,
+                               successCallback, errorCallback);
 }
 
 EntityReference Manager::preflight(
-    const EntityReference &entityReference, const trait::TraitSet &traitSet,
+    const EntityReference &entityReference, const TraitsDataPtr &traitsHint,
     const ContextConstPtr &context,
     [[maybe_unused]] const Manager::BatchElementErrorPolicyTag::Exception &errorPolicyTag) {
   EntityReference result{""};
   preflight(
-      {entityReference}, traitSet, context,
+      {entityReference}, {traitsHint}, context,
       [&result]([[maybe_unused]] std::size_t index, EntityReference preflightedRef) {
         result = std::move(preflightedRef);
       },
@@ -344,12 +352,12 @@ EntityReference Manager::preflight(
 }
 
 std::variant<BatchElementError, EntityReference> Manager::preflight(
-    const EntityReference &entityReference, const trait::TraitSet &traitSet,
+    const EntityReference &entityReference, const TraitsDataPtr &traitsHint,
     const ContextConstPtr &context,
     [[maybe_unused]] const Manager::BatchElementErrorPolicyTag::Variant &errorPolicyTag) {
   std::variant<BatchElementError, EntityReference> result;
   preflight(
-      {entityReference}, traitSet, context,
+      {entityReference}, {traitsHint}, context,
       [&result]([[maybe_unused]] std::size_t index, EntityReference preflightedRef) {
         result = std::move(preflightedRef);
       },
@@ -361,14 +369,14 @@ std::variant<BatchElementError, EntityReference> Manager::preflight(
 }
 
 EntityReferences Manager::preflight(
-    const EntityReferences &entityReferences, const trait::TraitSet &traitSet,
+    const EntityReferences &entityReferences, const trait::TraitsDatas &traitsHints,
     const ContextConstPtr &context,
     [[maybe_unused]] const Manager::BatchElementErrorPolicyTag::Exception &errorPolicyTag) {
   EntityReferences results;
   results.resize(entityReferences.size(), EntityReference{""});
 
   preflight(
-      entityReferences, traitSet, context,
+      entityReferences, traitsHints, context,
       [&results](std::size_t index, EntityReference preflightedRef) {
         results[index] = std::move(preflightedRef);
       },
@@ -381,13 +389,13 @@ EntityReferences Manager::preflight(
 }
 
 std::vector<std::variant<BatchElementError, EntityReference>> Manager::preflight(
-    const EntityReferences &entityReferences, const trait::TraitSet &traitSet,
+    const EntityReferences &entityReferences, const trait::TraitsDatas &traitsHints,
     const ContextConstPtr &context,
     [[maybe_unused]] const Manager::BatchElementErrorPolicyTag::Variant &errorPolicyTag) {
   std::vector<std::variant<BatchElementError, EntityReference>> results;
   results.resize(entityReferences.size());
   preflight(
-      entityReferences, traitSet, context,
+      entityReferences, traitsHints, context,
       [&results](std::size_t index, EntityReference entityReference) {
         results[index] = std::move(entityReference);
       },
@@ -404,23 +412,13 @@ void Manager::register_(const EntityReferences &entityReferences,
                         const RegisterSuccessCallback &successCallback,
                         const BatchElementErrorCallback &errorCallback) {
   if (entityReferences.size() != entityTraitsDatas.size()) {
-    throw std::out_of_range{"Parameter lists must be of the same length"};
+    std::string message = "Parameter lists must be of the same length: ";
+    message += std::to_string(entityReferences.size());
+    message += " entity references vs. ";
+    message += std::to_string(entityTraitsDatas.size());
+    message += " traits datas.";
+    throw std::out_of_range{message};
   }
-
-  if (!entityTraitsDatas.empty()) {
-    const trait::TraitSet firstTraitSet = entityTraitsDatas[0]->traitSet();
-    for (std::size_t idx = 1; idx < entityTraitsDatas.size(); ++idx) {
-      const trait::TraitSet currentTraitSet = entityTraitsDatas[idx]->traitSet();
-      if (currentTraitSet != firstTraitSet) {
-        Str msg = "Mismatched traits at index ";
-        msg += std::to_string(idx);
-        // TODO(DF): expand on error message to include actual trait
-        //  set.
-        throw std::invalid_argument{msg};
-      }
-    }
-  }
-
   return managerInterface_->register_(entityReferences, entityTraitsDatas, context, hostSession_,
                                       successCallback, errorCallback);
 }
diff --git a/src/openassetio-python/cmodule/src/BatchElementErrorBinding.cpp b/src/openassetio-python/cmodule/src/BatchElementErrorBinding.cpp
index 8a54507b9..9b4d0a47d 100644
--- a/src/openassetio-python/cmodule/src/BatchElementErrorBinding.cpp
+++ b/src/openassetio-python/cmodule/src/BatchElementErrorBinding.cpp
@@ -20,7 +20,8 @@ void registerBatchElementError(py::module &mod) {
       .value("kInvalidEntityReference", BatchElementError::ErrorCode::kInvalidEntityReference)
       .value("kMalformedEntityReference", BatchElementError::ErrorCode::kMalformedEntityReference)
       .value("kEntityAccessError", BatchElementError::ErrorCode::kEntityAccessError)
-      .value("kEntityResolutionError", BatchElementError::ErrorCode::kEntityResolutionError);
+      .value("kEntityResolutionError", BatchElementError::ErrorCode::kEntityResolutionError)
+      .value("kInvalidPreflightHint", BatchElementError::ErrorCode::kInvalidPreflightHint);
 
   batchElementError
       .def(py::init<BatchElementError::ErrorCode, openassetio::Str>(), py::arg("code"),
@@ -63,6 +64,7 @@ class BatchElementException(RuntimeError):
   using openassetio::EntityAccessErrorBatchElementException;
   using openassetio::EntityResolutionErrorBatchElementException;
   using openassetio::InvalidEntityReferenceBatchElementException;
+  using openassetio::InvalidPreflightHintBatchElementException;
   using openassetio::MalformedEntityReferenceBatchElementException;
   using openassetio::UnknownBatchElementException;
 
@@ -80,6 +82,7 @@ class BatchElementException(RuntimeError):
   registerPyException("MalformedEntityReferenceBatchElementException");
   registerPyException("EntityAccessErrorBatchElementException");
   registerPyException("EntityResolutionErrorBatchElementException");
+  registerPyException("InvalidPreflightHintBatchElementException");
 
   // Register a function that will translate our C++ exceptions to the
   // appropriate Python exception type.
@@ -115,6 +118,8 @@ class BatchElementException(RuntimeError):
       setPyException("EntityAccessErrorBatchElementException", exc);
     } catch (const EntityResolutionErrorBatchElementException &exc) {
       setPyException("EntityResolutionErrorBatchElementException", exc);
+    } catch (const InvalidPreflightHintBatchElementException &exc) {
+      setPyException("InvalidPreflightHintBatchElementException", exc);
     } catch (const BatchElementException &exc) {
       setPyException("BatchElementException", exc);
     }
diff --git a/src/openassetio-python/cmodule/src/hostApi/ManagerBinding.cpp b/src/openassetio-python/cmodule/src/hostApi/ManagerBinding.cpp
index 158e00c8f..cef4b802b 100644
--- a/src/openassetio-python/cmodule/src/hostApi/ManagerBinding.cpp
+++ b/src/openassetio-python/cmodule/src/hostApi/ManagerBinding.cpp
@@ -180,51 +180,65 @@ void registerManager(const py::module& mod) {
           py::arg("entityReference"), py::arg("relationshipTraitsDatas"), py::arg("pageSize"),
           py::arg("context").none(false), py::arg("successCallback"), py::arg("errorCallback"),
           py::arg("resultTraitSet") = trait::TraitSet{})
+      .def(
+          "preflight",
+          [](Manager& self, const EntityReferences& entityReferences,
+             const trait::TraitsDatas& traitsHints, const ContextConstPtr& context,
+             const Manager::PreflightSuccessCallback& successCallback,
+             const Manager::BatchElementErrorCallback& errorCallback) {
+            validateTraitsDatas(traitsHints);
+            self.preflight(entityReferences, traitsHints, context, successCallback, errorCallback);
+          },
+          py::arg("entityReferences"), py::arg("traitsHints"), py::arg("context").none(false),
+          py::arg("successCallback"), py::arg("errorCallback"))
       .def("preflight",
-           static_cast<void (Manager::*)(
-               const EntityReferences&, const trait::TraitSet&, const ContextConstPtr&,
-               const Manager::PreflightSuccessCallback&,
-               const Manager::BatchElementErrorCallback&)>(&Manager::preflight),
-           py::arg("entityReferences"), py::arg("traitSet"), py::arg("context").none(false),
-           py::arg("successCallback"), py::arg("errorCallback"))
-      .def("preflight",
-           static_cast<EntityReference (Manager::*)(
-               const EntityReference&, const trait::TraitSet&, const ContextConstPtr&,
-               const Manager::BatchElementErrorPolicyTag::Exception&)>(&Manager::preflight),
-           py::arg("entityReference"), py::arg("traitSet"), py::arg("context").none(false),
-           py::arg("errorPolicyTag"))
+           py::overload_cast<const EntityReference&, const TraitsDataPtr&, const ContextConstPtr&,
+                             const Manager::BatchElementErrorPolicyTag::Exception&>(
+               &Manager::preflight),
+           py::arg("entityReference"), py::arg("traitsHint").none(false),
+           py::arg("context").none(false), py::arg("errorPolicyTag"))
       .def("preflight",
-           static_cast<std::variant<BatchElementError, EntityReference> (Manager::*)(
-               const EntityReference&, const trait::TraitSet&, const ContextConstPtr&,
-               const Manager::BatchElementErrorPolicyTag::Variant&)>(&Manager::preflight),
-           py::arg("entityReference"), py::arg("traitSet"), py::arg("context").none(false),
-           py::arg("errorPolicyTag"))
+           py::overload_cast<const EntityReference&, const TraitsDataPtr&, const ContextConstPtr&,
+                             const Manager::BatchElementErrorPolicyTag::Variant&>(
+               &Manager::preflight),
+           py::arg("entityReference"), py::arg("traitsHint").none(false),
+           py::arg("context").none(false), py::arg("errorPolicyTag"))
       .def(
           "preflight",
           [](Manager& self, const EntityReference& entityReference,
-             const trait::TraitSet& traitSet, const ContextConstPtr& context) {
-            return self.preflight(entityReference, traitSet, context);
+             const TraitsDataPtr& traitsHint, const ContextConstPtr& context) {
+            return self.preflight(entityReference, traitsHint, context);
           },
-          py::arg("entityReference"), py::arg("traitSet"), py::arg("context").none(false))
-      .def("preflight",
-           static_cast<std::vector<EntityReference> (Manager::*)(
-               const EntityReferences&, const trait::TraitSet&, const ContextConstPtr&,
-               const Manager::BatchElementErrorPolicyTag::Exception&)>(&Manager::preflight),
-           py::arg("entityReferences"), py::arg("traitSet"), py::arg("context").none(false),
-           py::arg("errorPolicyTag"))
-      .def("preflight",
-           static_cast<std::vector<std::variant<BatchElementError, EntityReference>> (Manager::*)(
-               const EntityReferences&, const trait::TraitSet&, const ContextConstPtr&,
-               const Manager::BatchElementErrorPolicyTag::Variant&)>(&Manager::preflight),
-           py::arg("entityReferences"), py::arg("traitSet"), py::arg("context").none(false),
-           py::arg("errorPolicyTag"))
+          py::arg("entityReference"), py::arg("traitsHint").none(false),
+          py::arg("context").none(false))
       .def(
           "preflight",
           [](Manager& self, const EntityReferences& entityReferences,
-             const trait::TraitSet& traitSet, const ContextConstPtr& context) {
-            return self.preflight(entityReferences, traitSet, context);
+             const trait::TraitsDatas& traitsHints, const ContextConstPtr& context,
+             const Manager::BatchElementErrorPolicyTag::Exception& tag) {
+            validateTraitsDatas(traitsHints);
+            return self.preflight(entityReferences, traitsHints, context, tag);
           },
-          py::arg("entityReferences"), py::arg("traitSet"), py::arg("context").none(false))
+          py::arg("entityReferences"), py::arg("traitsHints"), py::arg("context").none(false),
+          py::arg("errorPolicyTag"))
+      .def(
+          "preflight",
+          [](Manager& self, const EntityReferences& entityReferences,
+             const trait::TraitsDatas& traitsHints, const ContextConstPtr& context,
+             const Manager::BatchElementErrorPolicyTag::Variant& tag) {
+            validateTraitsDatas(traitsHints);
+            return self.preflight(entityReferences, traitsHints, context, tag);
+          },
+          py::arg("entityReferences"), py::arg("traitsHints"), py::arg("context").none(false),
+          py::arg("errorPolicyTag"))
+      .def(
+          "preflight",
+          [](Manager& self, const EntityReferences& entityReferences,
+             const trait::TraitsDatas& traitsHints, const ContextConstPtr& context) {
+            validateTraitsDatas(traitsHints);
+            return self.preflight(entityReferences, traitsHints, context);
+          },
+          py::arg("entityReferences"), py::arg("traitsHints"), py::arg("context").none(false))
       .def(
           "register",
           [](Manager& self, const EntityReferences& entityReferences,
diff --git a/src/openassetio-python/cmodule/src/managerApi/ManagerInterfaceBinding.cpp b/src/openassetio-python/cmodule/src/managerApi/ManagerInterfaceBinding.cpp
index 357eff712..fb9a557cf 100644
--- a/src/openassetio-python/cmodule/src/managerApi/ManagerInterfaceBinding.cpp
+++ b/src/openassetio-python/cmodule/src/managerApi/ManagerInterfaceBinding.cpp
@@ -146,12 +146,12 @@ struct PyManagerInterface : ManagerInterface {
         RetainCommonPyArgs::forFn(successCallback), errorCallback);
   }
 
-  void preflight(const EntityReferences& entityReferences, const trait::TraitSet& traitSet,
+  void preflight(const EntityReferences& entityReferences, const trait::TraitsDatas& traitsHints,
                  const ContextConstPtr& context, const HostSessionPtr& hostSession,
                  const PreflightSuccessCallback& successCallback,
                  const BatchElementErrorCallback& errorCallback) override {
-    PYBIND11_OVERRIDE_PURE(void, ManagerInterface, preflight, entityReferences, traitSet, context,
-                           hostSession, successCallback, errorCallback);
+    PYBIND11_OVERRIDE_PURE(void, ManagerInterface, preflight, entityReferences, traitsHints,
+                           context, hostSession, successCallback, errorCallback);
   }
 
   void register_(const EntityReferences& entityReferences, const trait::TraitsDatas& traitsDatas,
@@ -226,8 +226,9 @@ void registerManagerInterface(const py::module& mod) {
            py::arg("hostSession").none(false), py::arg("successCallback"),
            py::arg("errorCallback"))
       .def("preflight", &ManagerInterface::preflight, py::arg("entityReferences"),
-           py::arg("traitSet"), py::arg("context").none(false), py::arg("hostSession").none(false),
-           py::arg("successCallback"), py::arg("errorCallback"))
+           py::arg("traitsHints"), py::arg("context").none(false),
+           py::arg("hostSession").none(false), py::arg("successCallback"),
+           py::arg("errorCallback"))
       .def("register", &ManagerInterface::register_, py::arg("entityReferences"),
            py::arg("entityTraitsDatas"), py::arg("context").none(false),
            py::arg("hostSession").none(false), py::arg("successCallback"),
diff --git a/src/openassetio-python/package/openassetio/__init__.py b/src/openassetio-python/package/openassetio/__init__.py
index 9855b8c75..f22695abc 100644
--- a/src/openassetio-python/package/openassetio/__init__.py
+++ b/src/openassetio-python/package/openassetio/__init__.py
@@ -83,6 +83,7 @@
     MalformedEntityReferenceBatchElementException,
     EntityAccessErrorBatchElementException,
     EntityResolutionErrorBatchElementException,
+    InvalidPreflightHintBatchElementException,
 )
 
 # pylint: disable=wrong-import-position


Test patch:
diff --git a/src/openassetio-core/tests/hostApi/ManagerTest.cpp b/src/openassetio-core/tests/hostApi/ManagerTest.cpp
index 0df789eee..69a8d1484 100644
--- a/src/openassetio-core/tests/hostApi/ManagerTest.cpp
+++ b/src/openassetio-core/tests/hostApi/ManagerTest.cpp
@@ -408,7 +408,16 @@ SCENARIO("Preflighting entities") {
   using trompeloeil::_;
 
   GIVEN("a configured Manager instance") {
-    const openassetio::trait::TraitSet traits = {"fakeTrait", "secondFakeTrait"};
+    const openassetio::EntityReference ref = openassetio::EntityReference{"testReference"};
+    const openassetio::EntityReferences threeRefs = {
+        openassetio::EntityReference{"testReference1"},
+        openassetio::EntityReference{"testReference2"},
+        openassetio::EntityReference{"testReference3"}};
+
+    const openassetio::TraitsDataPtr traitsData =
+        openassetio::TraitsData::make({"fakeTrait", "secondFakeTrait"});
+    const openassetio::trait::TraitsDatas threeTraitsDatas{traitsData, traitsData, traitsData};
+
     const openassetio::ManagerFixture fixture;
     const auto& manager = fixture.manager;
     auto& mockManagerInterface = fixture.mockManagerInterface;
@@ -416,27 +425,26 @@ SCENARIO("Preflighting entities") {
     const auto& hostSession = fixture.hostSession;
 
     GIVEN("manager plugin successfully preflights a single entity reference") {
-      const openassetio::EntityReference ref{"testReference"};
-      const openassetio::EntityReferences refs = {ref};
-
       const openassetio::EntityReference expected{"preflightedRef"};
 
       // With success callback side effect
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface, preflight(openassetio::EntityReferences{ref},
+                                                   openassetio::trait::TraitsDatas{traitsData},
+                                                   context, hostSession, _, _))
           .LR_SIDE_EFFECT(_5(0, expected));
 
       WHEN("singular preflight is called with default errorPolicyTag") {
-        const openassetio::EntityReference actual = manager->preflight(ref, traits, context);
+        const openassetio::EntityReference actual = manager->preflight(ref, traitsData, context);
         THEN("returned entity reference is as expected") { CHECK(expected == actual); }
       }
       WHEN("singular preflight is called with kException errorPolicyTag") {
         const openassetio::EntityReference actual = manager->preflight(
-            ref, traits, context, hostApi::Manager::BatchElementErrorPolicyTag::kException);
+            ref, traitsData, context, hostApi::Manager::BatchElementErrorPolicyTag::kException);
         THEN("returned entity reference is as expected") { CHECK(expected == actual); }
       }
       WHEN("singular preflight is called with kVariant errorPolicyTag") {
         std::variant<openassetio::BatchElementError, openassetio::EntityReference> actual =
-            manager->preflight(ref, traits, context,
+            manager->preflight(ref, traitsData, context,
                                hostApi::Manager::BatchElementErrorPolicyTag::kVariant);
         THEN("returned variant contains the expected entity reference") {
           CHECK(std::holds_alternative<openassetio::EntityReference>(actual));
@@ -446,38 +454,36 @@ SCENARIO("Preflighting entities") {
       }
     }
     GIVEN("manager plugin successfully preflights multiple entity references") {
-      const openassetio::EntityReferences refs = {openassetio::EntityReference{"testReference1"},
-                                                  openassetio::EntityReference{"testReference2"},
-                                                  openassetio::EntityReference{"testReference3"}};
-
       const openassetio::EntityReference expected1{"ref1"};
       const openassetio::EntityReference expected2{"ref2"};
       const openassetio::EntityReference expected3{"ref3"};
       std::vector<openassetio::EntityReference> expectedVec{expected1, expected2, expected3};
 
       // With success callback side effect
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface,
+                   preflight(threeRefs, threeTraitsDatas, context, hostSession, _, _))
           .LR_SIDE_EFFECT(_5(0, expectedVec[0]))
           .LR_SIDE_EFFECT(_5(1, expectedVec[1]))
           .LR_SIDE_EFFECT(_5(2, expectedVec[2]));
 
       WHEN("batch preflight is called with default errorPolicyTag") {
         const std::vector<openassetio::EntityReference> actualVec =
-            manager->preflight(refs, traits, context);
+            manager->preflight(threeRefs, threeTraitsDatas, context);
         THEN("returned list of entity references is as expected") {
           CHECK(expectedVec == actualVec);
         }
       }
       WHEN("batch preflight is called with kException errorPolicyTag") {
-        const std::vector<openassetio::EntityReference> actualVec = manager->preflight(
-            refs, traits, context, hostApi::Manager::BatchElementErrorPolicyTag::kException);
+        const std::vector<openassetio::EntityReference> actualVec =
+            manager->preflight(threeRefs, threeTraitsDatas, context,
+                               hostApi::Manager::BatchElementErrorPolicyTag::kException);
         THEN("returned list of entity references is as expected") {
           CHECK(expectedVec == actualVec);
         }
       }
       WHEN("batch preflight is called with kVariant errorPolicyTag") {
         std::vector<std::variant<openassetio::BatchElementError, openassetio::EntityReference>>
-            actualVec = manager->preflight(refs, traits, context,
+            actualVec = manager->preflight(threeRefs, threeTraitsDatas, context,
                                            hostApi::Manager::BatchElementErrorPolicyTag::kVariant);
         THEN("returned lists of variants contains the expected entity references") {
           CHECK(expectedVec.size() == actualVec.size());
@@ -491,38 +497,36 @@ SCENARIO("Preflighting entities") {
     }
     GIVEN(
         "manager plugin successfully preflights multiple entity references in a non-index order") {
-      const openassetio::EntityReferences refs = {openassetio::EntityReference{"testReference1"},
-                                                  openassetio::EntityReference{"testReference2"},
-                                                  openassetio::EntityReference{"testReference3"}};
-
       const openassetio::EntityReference expected1{"ref1"};
       const openassetio::EntityReference expected2{"ref2"};
       const openassetio::EntityReference expected3{"ref3"};
       std::vector<openassetio::EntityReference> expectedVec{expected1, expected2, expected3};
 
       // With success callback side effect, given out of order.
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface,
+                   preflight(threeRefs, threeTraitsDatas, context, hostSession, _, _))
           .LR_SIDE_EFFECT(_5(2, expectedVec[2]))
           .LR_SIDE_EFFECT(_5(0, expectedVec[0]))
           .LR_SIDE_EFFECT(_5(1, expectedVec[1]));
 
       WHEN("batch preflight is called with default errorPolicyTag") {
         const std::vector<openassetio::EntityReference> actualVec =
-            manager->preflight(refs, traits, context);
+            manager->preflight(threeRefs, threeTraitsDatas, context);
         THEN("returned list of entity references is ordered in index order") {
           CHECK(expectedVec == actualVec);
         }
       }
       WHEN("batch preflight is called with kException errorPolicyTag") {
-        const std::vector<openassetio::EntityReference> actualVec = manager->preflight(
-            refs, traits, context, hostApi::Manager::BatchElementErrorPolicyTag::kException);
+        const std::vector<openassetio::EntityReference> actualVec =
+            manager->preflight(threeRefs, threeTraitsDatas, context,
+                               hostApi::Manager::BatchElementErrorPolicyTag::kException);
         THEN("returned list of entity references is ordered in index order") {
           CHECK(expectedVec == actualVec);
         }
       }
       WHEN("batch preflight is called with kVariant errorPolicyTag") {
         std::vector<std::variant<openassetio::BatchElementError, openassetio::EntityReference>>
-            actualVec = manager->preflight(refs, traits, context,
+            actualVec = manager->preflight(threeRefs, threeTraitsDatas, context,
                                            hostApi::Manager::BatchElementErrorPolicyTag::kVariant);
         THEN("returned lists of variants is ordered in index order") {
           CHECK(expectedVec.size() == actualVec.size());
@@ -537,19 +541,18 @@ SCENARIO("Preflighting entities") {
     GIVEN(
         "manager plugin will encounter an entity-specific error when next preflighting a "
         "reference") {
-      const openassetio::EntityReference ref = openassetio::EntityReference{"testReference"};
-      const openassetio::EntityReferences refs = {ref};
-
-      openassetio::BatchElementError expected{
+      const openassetio::BatchElementError expected{
           openassetio::BatchElementError::ErrorCode::kMalformedEntityReference, "Error Message"};
 
       // With error callback side effect
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface, preflight(openassetio::EntityReferences{ref},
+                                                   openassetio::trait::TraitsDatas{traitsData},
+                                                   context, hostSession, _, _))
           .LR_SIDE_EFFECT(_6(0, expected));
 
       WHEN("singular preflight is called with default errorPolicyTag") {
         THEN("an exception is thrown") {
-          CHECK_THROWS_MATCHES(manager->preflight(ref, traits, context),
+          CHECK_THROWS_MATCHES(manager->preflight(ref, traitsData, context),
                                openassetio::MalformedEntityReferenceBatchElementException,
                                Catch::Message("Error Message"));
         }
@@ -557,7 +560,7 @@ SCENARIO("Preflighting entities") {
       WHEN("singular preflight is called with kException errorPolicyTag") {
         THEN("an exception is thrown") {
           CHECK_THROWS_MATCHES(
-              manager->preflight(ref, traits, context,
+              manager->preflight(ref, traitsData, context,
                                  hostApi::Manager::BatchElementErrorPolicyTag::kException),
               openassetio::MalformedEntityReferenceBatchElementException,
               Catch::Message("Error Message"));
@@ -565,7 +568,7 @@ SCENARIO("Preflighting entities") {
       }
       WHEN("singular preflight is called with kVariant errorPolicyTag") {
         std::variant<openassetio::BatchElementError, openassetio::EntityReference> actual =
-            manager->preflight(ref, traits, context,
+            manager->preflight(ref, traitsData, context,
                                hostApi::Manager::BatchElementErrorPolicyTag::kVariant);
         THEN("returned variant contains the expected BatchElementError") {
           CHECK(std::holds_alternative<openassetio::BatchElementError>(actual));
@@ -577,10 +580,6 @@ SCENARIO("Preflighting entities") {
     GIVEN(
         "manager plugin will encounter entity-specific errors when next preflighting multiple "
         "references") {
-      const openassetio::EntityReferences refs = {openassetio::EntityReference{"testReference1"},
-                                                  openassetio::EntityReference{"testReference2"},
-                                                  openassetio::EntityReference{"testReference3"}};
-
       const openassetio::EntityReference expectedValue2{"ref2"};
       const openassetio::BatchElementError expectedError0{
           openassetio::BatchElementError::ErrorCode::kMalformedEntityReference,
@@ -590,14 +589,15 @@ SCENARIO("Preflighting entities") {
           "Entity Access Error Message"};
 
       // With mixed callback side effect
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface,
+                   preflight(threeRefs, threeTraitsDatas, context, hostSession, _, _))
           .LR_SIDE_EFFECT(_5(2, expectedValue2))
           .LR_SIDE_EFFECT(_6(0, expectedError0))
           .LR_SIDE_EFFECT(_6(1, expectedError1));
 
       WHEN("batch preflight is called with default errorPolicyTag") {
         THEN("an exception is thrown") {
-          CHECK_THROWS_MATCHES(manager->preflight(refs, traits, context),
+          CHECK_THROWS_MATCHES(manager->preflight(threeRefs, threeTraitsDatas, context),
                                openassetio::MalformedEntityReferenceBatchElementException,
                                Catch::Message("Malformed Mock Error🤖"));
         }
@@ -605,7 +605,7 @@ SCENARIO("Preflighting entities") {
       WHEN("batch preflight is called with kException errorPolicyTag") {
         THEN("an exception is thrown") {
           CHECK_THROWS_MATCHES(
-              manager->preflight(refs, traits, context,
+              manager->preflight(threeRefs, threeTraitsDatas, context,
                                  hostApi::Manager::BatchElementErrorPolicyTag::kException),
               openassetio::MalformedEntityReferenceBatchElementException,
               Catch::Message("Malformed Mock Error🤖"));
@@ -613,7 +613,7 @@ SCENARIO("Preflighting entities") {
       }
       WHEN("batch preflight is called with kVariant errorPolicyTag") {
         std::vector<std::variant<openassetio::BatchElementError, openassetio::EntityReference>>
-            actualVec = manager->preflight(refs, traits, context,
+            actualVec = manager->preflight(threeRefs, threeTraitsDatas, context,
                                            hostApi::Manager::BatchElementErrorPolicyTag::kVariant);
         THEN("returned lists of variants contains the expected objects") {
           auto error0 = std::get<openassetio::BatchElementError>(actualVec[0]);
@@ -647,7 +647,16 @@ TEMPLATE_TEST_CASE(
   static constexpr ErrorCode kExpectedErrorCode = TestType::kErrorCode;
 
   GIVEN("a configured Manager instance") {
-    const openassetio::trait::TraitSet traits = {"fakeTrait", "secondFakeTrait"};
+    const openassetio::EntityReference ref = openassetio::EntityReference{"testReference"};
+    const openassetio::EntityReferences threeRefs = {
+        openassetio::EntityReference{"testReference1"},
+        openassetio::EntityReference{"testReference2"},
+        openassetio::EntityReference{"testReference3"}};
+
+    const openassetio::TraitsDataPtr traitsData =
+        openassetio::TraitsData::make({"fakeTrait", "secondFakeTrait"});
+    const openassetio::trait::TraitsDatas threeTraitsDatas{traitsData, traitsData, traitsData};
+
     const openassetio::ManagerFixture fixture;
     const auto& manager = fixture.manager;
     auto& mockManagerInterface = fixture.mockManagerInterface;
@@ -657,19 +666,18 @@ TEMPLATE_TEST_CASE(
     AND_GIVEN(
         "manager plugin will encounter an entity-specific error when next preflighting a "
         "reference") {
-      const openassetio::EntityReference ref = openassetio::EntityReference{"testReference"};
-      const openassetio::EntityReferences refs = {ref};
-
       const openassetio::BatchElementError expectedError{kExpectedErrorCode, "Some error message"};
 
       // With error callback side effect
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface, preflight(openassetio::EntityReferences{ref},
+                                                   openassetio::trait::TraitsDatas{traitsData},
+                                                   context, hostSession, _, _))
           .LR_SIDE_EFFECT(_6(123, expectedError));
 
       WHEN("preflight is called with kException errorPolicyTag") {
         THEN("an exception is thrown") {
           try {
-            manager->preflight(ref, traits, context,
+            manager->preflight(ref, traitsData, context,
                                hostApi::Manager::BatchElementErrorPolicyTag::kException);
             FAIL_CHECK("Exception not thrown");
           } catch (const ExpectedExceptionType& exc) {
@@ -684,20 +692,18 @@ TEMPLATE_TEST_CASE(
     AND_GIVEN(
         "manager plugin will encounter entity-specific errors when next preflighting multiple "
         "references") {
-      const openassetio::EntityReferences refs = {openassetio::EntityReference{"testReference1"},
-                                                  openassetio::EntityReference{"testReference2"}};
-
       const openassetio::BatchElementError expectedError{kExpectedErrorCode, "Some error"};
 
       // With error callback side effect
-      REQUIRE_CALL(mockManagerInterface, preflight(refs, traits, context, hostSession, _, _))
+      REQUIRE_CALL(mockManagerInterface,
+                   preflight(threeRefs, threeTraitsDatas, context, hostSession, _, _))
           .LR_SIDE_EFFECT(_6(123, expectedError))
           .LR_SIDE_EFFECT(FAIL_CHECK("Exception should have short-circuited this"));
 
       WHEN("preflight is called with kException errorPolicyTag") {
         THEN("an exception is thrown") {
           try {
-            manager->preflight(refs, traits, context,
+            manager->preflight(threeRefs, threeTraitsDatas, context,
                                hostApi::Manager::BatchElementErrorPolicyTag::kException);
             FAIL_CHECK("Exception not thrown");
           } catch (const ExpectedExceptionType& exc) {
@@ -864,7 +870,7 @@ SCENARIO("Registering entities") {
       const openassetio::EntityReference ref = openassetio::EntityReference{"testReference"};
       const openassetio::EntityReferences refs = {ref};
 
-      openassetio::BatchElementError expected{
+      const openassetio::BatchElementError expected{
           openassetio::BatchElementError::ErrorCode::kMalformedEntityReference, "Error Message"};
 
       // With error callback side effect
diff --git a/src/openassetio-python/package/openassetio/test/manager/apiComplianceSuite.py b/src/openassetio-python/package/openassetio/test/manager/apiComplianceSuite.py
index 807f6378c..7df836ee5 100644
--- a/src/openassetio-python/package/openassetio/test/manager/apiComplianceSuite.py
+++ b/src/openassetio-python/package/openassetio/test/manager/apiComplianceSuite.py
@@ -389,19 +389,14 @@ class Test_preflight(FixtureAugmentedTestCase):
     managerApi.ManagerInterface.preflight.
     """
 
-    def setUp(self):
-        self.a_reference_to_a_writable_entity = self._manager.createEntityReference(
-            self.requireFixture("a_reference_to_a_writable_entity", skipTestIfMissing=True)
-        )
-        self.collectRequiredFixture("a_set_of_valid_traits")
-
     def test_when_multiple_references_then_same_number_of_returned_references(self):
-        ref = self.a_reference_to_a_writable_entity
+        traits_data = self.requireFixture("a_traits_data_for_preflight", skipTestIfMissing=True)
+        entity_reference = self.requireEntityReferenceFixture("a_reference_for_preflight")
 
         results = []
         self._manager.preflight(
-            [ref, ref],
-            self.a_set_of_valid_traits,
+            [entity_reference, entity_reference],
+            [traits_data, traits_data],
             self.createTestContext(),
             lambda _, ref: results.append(ref),
             lambda _, err: self.fail(err.message),
@@ -412,30 +407,52 @@ def test_when_multiple_references_then_same_number_of_returned_references(self):
             self.assertIsInstance(result, EntityReference)
 
     def test_when_reference_is_read_only_then_access_error_is_returned(self):
+        traits_data = self.requireFixture("a_traits_data_for_preflight", skipTestIfMissing=True)
+        entity_reference = self.requireEntityReferenceFixture("a_reference_to_a_readonly_entity")
+        expected_error_message = self.requireFixture("expected_error_message")
         self.__testPreflightError(
-            "a_reference_to_a_readonly_entity", BatchElementError.ErrorCode.kEntityAccessError
+            entity_reference,
+            traits_data,
+            expected_error_message,
+            BatchElementError.ErrorCode.kEntityAccessError,
         )
 
     def test_when_reference_malformed_then_malformed_entity_reference_error_returned(self):
+        traits_data = self.requireFixture("a_traits_data_for_preflight", skipTestIfMissing=True)
+        entity_reference = self.requireEntityReferenceFixture("a_malformed_reference")
+        expected_error_message = self.requireFixture("expected_error_message")
         self.__testPreflightError(
-            "a_malformed_reference", BatchElementError.ErrorCode.kMalformedEntityReference
+            entity_reference,
+            traits_data,
+            expected_error_message,
+            BatchElementError.ErrorCode.kMalformedEntityReference,
         )
 
-    def __testPreflightError(self, fixture_name, errorCode):
-        reference = self._manager.createEntityReference(
-            self.requireFixture(fixture_name, skipTestIfMissing=True)
+    def test_when_preflight_hint_invalid_then_invalid_preflight_hint_error_returned(self):
+        traits_data = self.requireFixture(
+            "an_invalid_traits_data_for_preflight", skipTestIfMissing=True
+        )
+        entity_reference = self.requireEntityReferenceFixture("a_reference_for_preflight")
+        expected_error_message = self.requireFixture("expected_error_message")
+        self.__testPreflightError(
+            entity_reference,
+            traits_data,
+            expected_error_message,
+            BatchElementError.ErrorCode.kInvalidPreflightHint,
         )
 
-        expected_msg = self.requireFixture(f"the_error_string_for_{fixture_name}")
-        expected_error = BatchElementError(errorCode, expected_msg)
+    def __testPreflightError(
+        self, entity_reference, traits_data, expected_error_message, expected_error_code
+    ):
+        expected_error = BatchElementError(expected_error_code, expected_error_message)
 
         context = self.createTestContext()
         context.access = Context.Access.kWrite
 
         errors = []
         self._manager.preflight(
-            [reference],
-            self.a_set_of_valid_traits,
+            [entity_reference],
+            [traits_data],
             context,
             lambda _idx, _ref: self.fail("Preflight should not succeed"),
             lambda _idx, error: errors.append(error),
diff --git a/src/openassetio-python/tests/conftest.py b/src/openassetio-python/tests/conftest.py
index e7dafcead..ee47a6234 100644
--- a/src/openassetio-python/tests/conftest.py
+++ b/src/openassetio-python/tests/conftest.py
@@ -171,15 +171,15 @@ def defaultEntityReference(self, traitSets, context, hostSession):
         return self.mock.defaultEntityReference(traitSets, context, hostSession)
 
     def preflight(
-        self, targetEntityRefs, traitSet, context, hostSession, successCallback, errorCallback
+        self, targetEntityRefs, traitsDatas, context, hostSession, successCallback, errorCallback
     ):
         self.__assertIsIterableOf(targetEntityRefs, EntityReference)
-        self.__assertIsIterableOf(traitSet, str)
+        self.__assertIsIterableOf(traitsDatas, TraitsData)
         self.__assertCallingContext(context, hostSession)
         assert callable(successCallback)
         assert callable(errorCallback)
         return self.mock.preflight(
-            targetEntityRefs, traitSet, context, hostSession, successCallback, errorCallback
+            targetEntityRefs, traitsDatas, context, hostSession, successCallback, errorCallback
         )
 
     def createState(self, hostSession):
diff --git a/src/openassetio-python/tests/package/hostApi/test_manager.py b/src/openassetio-python/tests/package/hostApi/test_manager.py
index de7712397..479de7197 100644
--- a/src/openassetio-python/tests/package/hostApi/test_manager.py
+++ b/src/openassetio-python/tests/package/hostApi/test_manager.py
@@ -1204,14 +1204,14 @@ def test_when_success_then_multiple_TraitsDatas_returned(
         some_refs,
         an_entity_trait_set,
         a_context,
-        some_populated_traitsdatas,
+        some_entity_traitsdatas,
         invoke_resolve_success_cb,
     ):
         method = mock_manager_interface.mock.resolve
 
         def call_callbacks(*_args):
-            invoke_resolve_success_cb(0, some_populated_traitsdatas[0])
-            invoke_resolve_success_cb(1, some_populated_traitsdatas[1])
+            invoke_resolve_success_cb(0, some_entity_traitsdatas[0])
+            invoke_resolve_success_cb(1, some_entity_traitsdatas[1])
 
         method.side_effect = call_callbacks
 
@@ -1227,8 +1227,8 @@ def call_callbacks(*_args):
         )
 
         assert len(actual_traitsdatas) == 2
-        assert actual_traitsdatas[0] is some_populated_traitsdatas[0]
-        assert actual_traitsdatas[1] is some_populated_traitsdatas[1]
+        assert actual_traitsdatas[0] is some_entity_traitsdatas[0]
+        assert actual_traitsdatas[1] is some_entity_traitsdatas[1]
 
     def test_when_success_out_of_order_then_TraitsDatas_returned_in_order(
         self,
@@ -1238,14 +1238,14 @@ def test_when_success_out_of_order_then_TraitsDatas_returned_in_order(
         some_refs,
         an_entity_trait_set,
         a_context,
-        some_populated_traitsdatas,
+        some_entity_traitsdatas,
         invoke_resolve_success_cb,
     ):
         method = mock_manager_interface.mock.resolve
 
         def call_callbacks(*_args):
-            invoke_resolve_success_cb(1, some_populated_traitsdatas[1])
-            invoke_resolve_success_cb(0, some_populated_traitsdatas[0])
+            invoke_resolve_success_cb(1, some_entity_traitsdatas[1])
+            invoke_resolve_success_cb(0, some_entity_traitsdatas[0])
 
         method.side_effect = call_callbacks
 
@@ -1261,8 +1261,8 @@ def call_callbacks(*_args):
         )
 
         assert len(actual_traitsdatas) == 2
-        assert actual_traitsdatas[0] is some_populated_traitsdatas[0]
-        assert actual_traitsdatas[1] is some_populated_traitsdatas[1]
+        assert actual_traitsdatas[0] is some_entity_traitsdatas[0]
+        assert actual_traitsdatas[1] is some_entity_traitsdatas[1]
 
     @pytest.mark.parametrize(
         "error_code,expected_exception", batch_element_error_code_exception_pairs
@@ -1318,14 +1318,14 @@ def test_when_success_then_multiple_TraitsDatas_returned(
         some_refs,
         an_entity_trait_set,
         a_context,
-        some_populated_traitsdatas,
+        some_entity_traitsdatas,
         invoke_resolve_success_cb,
     ):
         method = mock_manager_interface.mock.resolve
 
         def call_callbacks(*_args):
-            invoke_resolve_success_cb(0, some_populated_traitsdatas[0])
-            invoke_resolve_success_cb(1, some_populated_traitsdatas[1])
+            invoke_resolve_success_cb(0, some_entity_traitsdatas[0])
+            invoke_resolve_success_cb(1, some_entity_traitsdatas[1])
 
         method.side_effect = call_callbacks
 
@@ -1346,8 +1346,8 @@ def call_callbacks(*_args):
         )
 
         assert len(actual_traitsdatas) == 2
-        assert actual_traitsdatas[0] is some_populated_traitsdatas[0]
-        assert actual_traitsdatas[1] is some_populated_traitsdatas[1]
+        assert actual_traitsdatas[0] is some_entity_traitsdatas[0]
+        assert actual_traitsdatas[1] is some_entity_traitsdatas[1]
 
     def test_when_success_out_of_order_then_TraitsDatas_returned_in_order(
         self,
@@ -1357,15 +1357,15 @@ def test_when_success_out_of_order_then_TraitsDatas_returned_in_order(
         some_refs,
         an_entity_trait_set,
         a_context,
-        some_populated_traitsdatas,
+        some_entity_traitsdatas,
         invoke_resolve_success_cb,
     ):
         method = mock_manager_interface.mock.resolve
 
         def call_callbacks(*_args):
             # Success
-            invoke_resolve_success_cb(1, some_populated_traitsdatas[1])
-            invoke_resolve_success_cb(0, some_populated_traitsdatas[0])
+            invoke_resolve_success_cb(1, some_entity_traitsdatas[1])
+            invoke_resolve_success_cb(0, some_entity_traitsdatas[0])
 
         method.side_effect = call_callbacks
 
@@ -1386,8 +1386,8 @@ def call_callbacks(*_args):
         )
 
         assert len(actual_traitsdatas) == 2
-        assert actual_traitsdatas[0] is some_populated_traitsdatas[0]
-        assert actual_traitsdatas[1] is some_populated_traitsdatas[1]
+        assert actual_traitsdatas[0] is some_entity_traitsdatas[0]
+        assert actual_traitsdatas[1] is some_entity_traitsdatas[1]
 
     @pytest.mark.parametrize(
         "error_code,expected_exception", batch_element_error_code_exception_pairs
@@ -1571,7 +1571,7 @@ def test_wraps_the_corresponding_method_of_the_held_interface(
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         a_batch_element_error,
         invoke_preflight_success_cb,
@@ -1590,25 +1590,59 @@ def call_callbacks(*_args):
         method.side_effect = call_callbacks
 
         manager.preflight(
-            some_refs, an_entity_trait_set, a_context, success_callback, error_callback
+            some_refs, some_entity_traitsdatas, a_context, success_callback, error_callback
         )
 
         method.assert_called_once_with(
-            some_refs, an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            some_refs, some_entity_traitsdatas, a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         success_callback.assert_called_once_with(123, some_refs[0])
         error_callback.assert_called_once_with(456, a_batch_element_error)
 
+    def test_when_called_with_mixed_array_lengths_then_IndexError_is_raised(
+        self, manager, some_refs, some_entity_traitsdatas, a_context
+    ):
+        expected_message = (
+            "Parameter lists must be of the same length: {} entity references vs. {} traits hints."
+        )
+        with pytest.raises(
+            IndexError, match=expected_message.format(len(some_refs) - 1, len(some_refs))
+        ):
+            manager.preflight(
+                some_refs[1:], some_entity_traitsdatas, a_context, mock.Mock(), mock.Mock()
+            )
+
+        with pytest.raises(
+            IndexError, match=expected_message.format(len(some_refs), len(some_refs) - 1)
+        ):
+            manager.preflight(
+                some_refs, some_entity_traitsdatas[1:], a_context, mock.Mock(), mock.Mock()
+            )
+
+    def test_when_traits_data_is_None_then_TypeError_is_raised(
+        self, manager, some_refs, some_entity_traitsdatas, a_context
+    ):
+        some_entity_traitsdatas[-1] = None
+
+        with pytest.raises(TypeError):
+            manager.preflight(
+                some_refs, some_entity_traitsdatas, a_context, mock.Mock(), mock.Mock()
+            )
+
 
 class Test_Manager_preflight_with_singular_default_overload:
+    def test_when_traits_data_is_None_then_TypeError_is_raised(self, manager, a_ref, a_context):
+        with pytest.raises(TypeError):
+            manager.preflight(a_ref, None, a_context)
+
     def test_when_success_then_single_EntityReference_returned(
         self,
         manager,
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         a_different_ref,
         invoke_preflight_success_cb,
@@ -1620,10 +1654,10 @@ def call_callbacks(*_args):
 
         method.side_effect = call_callbacks
 
-        actual_ref = manager.preflight(a_ref, an_entity_trait_set, a_context)
+        actual_ref = manager.preflight(a_ref, a_traitsdata, a_context)
 
         method.assert_called_once_with(
-            [a_ref], an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            [a_ref], [a_traitsdata], a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         assert actual_ref == a_different_ref
@@ -1637,7 +1671,7 @@ def test_when_BatchElementError_then_appropriate_exception_raised(
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         error_code,
         expected_exception,
@@ -1655,10 +1689,10 @@ def call_callbacks(*_args):
         method.side_effect = call_callbacks
 
         with pytest.raises(expected_exception, match=batch_element_error.message) as exc:
-            manager.preflight(a_ref, an_entity_trait_set, a_context)
+            manager.preflight(a_ref, a_traitsdata, a_context)
 
         method.assert_called_once_with(
-            [a_ref], an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            [a_ref], [a_traitsdata], a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         assert exc.value.index == expected_index
@@ -1666,13 +1700,19 @@ def call_callbacks(*_args):
 
 
 class Test_Manager_preflight_with_singular_throwing_overload:
+    def test_when_traits_data_is_None_then_TypeError_is_raised(self, manager, a_ref, a_context):
+        with pytest.raises(TypeError):
+            manager.preflight(
+                a_ref, None, a_context, Manager.BatchElementErrorPolicyTag.kException
+            )
+
     def test_when_preflight_success_then_single_EntityReference_returned(
         self,
         manager,
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         a_different_ref,
         invoke_preflight_success_cb,
@@ -1686,13 +1726,13 @@ def call_callbacks(*_args):
 
         actual_ref = manager.preflight(
             a_ref,
-            an_entity_trait_set,
+            a_traitsdata,
             a_context,
             Manager.BatchElementErrorPolicyTag.kException,
         )
 
         method.assert_called_once_with(
-            [a_ref], an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            [a_ref], [a_traitsdata], a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         assert actual_ref == a_different_ref
@@ -1706,7 +1746,7 @@ def test_when_BatchElementError_then_appropriate_exception_raised(
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         error_code,
         expected_exception,
@@ -1727,13 +1767,13 @@ def call_callbacks(*_args):
         with pytest.raises(expected_exception, match=batch_element_error.message) as exc:
             manager.preflight(
                 a_ref,
-                an_entity_trait_set,
+                a_traitsdata,
                 a_context,
                 Manager.BatchElementErrorPolicyTag.kException,
             )
 
         method.assert_called_once_with(
-            [a_ref], an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            [a_ref], [a_traitsdata], a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         assert exc.value.index == expected_index
@@ -1741,13 +1781,17 @@ def call_callbacks(*_args):
 
 
 class Test_Manager_preflight_with_singular_variant_overload:
+    def test_when_traits_data_is_None_then_TypeError_is_raised(self, manager, a_ref, a_context):
+        with pytest.raises(TypeError):
+            manager.preflight(a_ref, None, a_context, Manager.BatchElementErrorPolicyTag.kVariant)
+
     def test_when_preflight_success_then_single_EntityReference_returned(
         self,
         manager,
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         a_different_ref,
         invoke_preflight_success_cb,
@@ -1761,13 +1805,13 @@ def call_callbacks(*_args):
 
         actual_ref = manager.preflight(
             a_ref,
-            an_entity_trait_set,
+            a_traitsdata,
             a_context,
             Manager.BatchElementErrorPolicyTag.kVariant,
         )
 
         method.assert_called_once_with(
-            [a_ref], an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            [a_ref], [a_traitsdata], a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         assert actual_ref == a_different_ref
@@ -1779,7 +1823,7 @@ def test_when_BatchElementError_then_BatchElementError_returned(
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         error_code,
         invoke_preflight_error_cb,
@@ -1796,26 +1840,33 @@ def call_callbacks(*_args):
 
         actual = manager.preflight(
             a_ref,
-            an_entity_trait_set,
+            a_traitsdata,
             a_context,
             Manager.BatchElementErrorPolicyTag.kVariant,
         )
 
         method.assert_called_once_with(
-            [a_ref], an_entity_trait_set, a_context, a_host_session, mock.ANY, mock.ANY
+            [a_ref], [a_traitsdata], a_context, a_host_session, mock.ANY, mock.ANY
         )
 
         assert actual == batch_element_error
 
 
 class Test_Manager_preflight_with_batch_default_overload:
+    def test_when_traits_data_is_None_then_TypeError_is_raised(
+        self, manager, some_refs, some_entity_traitsdatas, a_context
+    ):
+        some_entity_traitsdatas[-1] = None
+        with pytest.raises(TypeError):
+            manager.preflight(some_refs, some_entity_traitsdatas, a_context)
+
     def test_when_success_then_multiple_EntityReferences_returned(
         self,
         manager,
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         some_different_refs,
         invoke_preflight_success_cb,
@@ -1828,11 +1879,11 @@ def call_callbacks(*_args):
 
         method.side_effect = call_callbacks
 
-        actual_refs = manager.preflight(some_refs, an_entity_trait_set, a_context)
+        actual_refs = manager.preflight(some_refs, some_entity_traitsdatas, a_context)
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -1847,7 +1898,7 @@ def test_when_success_out_of_order_then_EntityReferences_returned_in_order(
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         some_different_refs,
         invoke_preflight_success_cb,
@@ -1860,11 +1911,11 @@ def call_callbacks(*_args):
 
         method.side_effect = call_callbacks
 
-        actual_refs = manager.preflight(some_refs, an_entity_trait_set, a_context)
+        actual_refs = manager.preflight(some_refs, some_entity_traitsdatas, a_context)
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -1882,7 +1933,7 @@ def test_when_BatchElementError_then_appropriate_exception_raised(
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         error_code,
         a_different_ref,
@@ -1903,11 +1954,11 @@ def call_callbacks(*_args):
         method.side_effect = call_callbacks
 
         with pytest.raises(expected_exception, match=batch_element_error.message) as exc:
-            manager.preflight(some_refs, an_entity_trait_set, a_context)
+            manager.preflight(some_refs, some_entity_traitsdatas, a_context)
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -1919,13 +1970,25 @@ def call_callbacks(*_args):
 
 
 class Test_Manager_preflight_with_batch_throwing_overload:
+    def test_when_traits_data_is_None_then_TypeError_is_raised(
+        self, manager, some_refs, some_entity_traitsdatas, a_context
+    ):
+        some_entity_traitsdatas[-1] = None
+        with pytest.raises(TypeError):
+            manager.preflight(
+                some_refs,
+                some_entity_traitsdatas,
+                a_context,
+                Manager.BatchElementErrorPolicyTag.kException,
+            )
+
     def test_when_success_then_multiple_EntityReferences_returned(
         self,
         manager,
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         some_different_refs,
         invoke_preflight_success_cb,
@@ -1940,14 +2003,14 @@ def call_callbacks(*_args):
 
         actual_refs = manager.preflight(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             Manager.BatchElementErrorPolicyTag.kException,
         )
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -1962,7 +2025,7 @@ def test_when_success_out_of_order_then_EntityReferences_returned_in_order(
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         some_different_refs,
         invoke_preflight_success_cb,
@@ -1977,14 +2040,14 @@ def call_callbacks(*_args):
 
         actual_refs = manager.preflight(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             Manager.BatchElementErrorPolicyTag.kException,
         )
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -2002,7 +2065,7 @@ def test_when_BatchElementError_then_appropriate_exception_raised(
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         error_code,
         a_different_ref,
@@ -2025,14 +2088,14 @@ def call_callbacks(*_args):
         with pytest.raises(expected_exception, match=batch_element_error.message) as exc:
             manager.preflight(
                 some_refs,
-                an_entity_trait_set,
+                some_entity_traitsdatas,
                 a_context,
                 Manager.BatchElementErrorPolicyTag.kException,
             )
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -2044,6 +2107,18 @@ def call_callbacks(*_args):
 
 
 class Test_Manager_preflight_with_batch_variant_overload:
+    def test_when_traits_data_is_None_then_TypeError_is_raised(
+        self, manager, some_refs, some_entity_traitsdatas, a_context
+    ):
+        some_entity_traitsdatas[-1] = None
+        with pytest.raises(TypeError):
+            manager.preflight(
+                some_refs,
+                some_entity_traitsdatas,
+                a_context,
+                Manager.BatchElementErrorPolicyTag.kVariant,
+            )
+
     @pytest.mark.parametrize("error_code", batch_element_error_codes)
     def test_when_mixed_output_then_returned_list_contains_output(
         self,
@@ -2051,7 +2126,7 @@ def test_when_mixed_output_then_returned_list_contains_output(
         mock_manager_interface,
         a_host_session,
         some_refs,
-        an_entity_trait_set,
+        some_entity_traitsdatas,
         a_context,
         a_different_ref,
         error_code,
@@ -2069,14 +2144,14 @@ def call_callbacks(*_args):
 
         actual_ref_or_error = manager.preflight(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             Manager.BatchElementErrorPolicyTag.kVariant,
         )
 
         method.assert_called_once_with(
             some_refs,
-            an_entity_trait_set,
+            some_entity_traitsdatas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -2093,7 +2168,7 @@ def test_when_mixed_output_out_of_order_then_output_returned_in_order(
         mock_manager_interface,
         a_host_session,
         a_ref,
-        an_entity_trait_set,
+        a_traitsdata,
         a_context,
         invoke_preflight_success_cb,
         invoke_preflight_error_cb,
@@ -2101,6 +2176,7 @@ def test_when_mixed_output_out_of_order_then_output_returned_in_order(
         method = mock_manager_interface.mock.preflight
 
         entity_refs = [a_ref] * 4
+        traits_datas = [a_traitsdata] * len(entity_refs)
 
         batch_element_error0 = BatchElementError(
             BatchElementError.ErrorCode.kEntityResolutionError, "0 some string ✨"
@@ -2121,14 +2197,14 @@ def call_callbacks(*_args):
 
         actual_ref_or_error = manager.preflight(
             entity_refs,
-            an_entity_trait_set,
+            traits_datas,
             a_context,
             Manager.BatchElementErrorPolicyTag.kVariant,
         )
 
         method.assert_called_once_with(
             entity_refs,
-            an_entity_trait_set,
+            traits_datas,
             a_context,
             a_host_session,
             mock.ANY,
@@ -2183,23 +2259,24 @@ def call_callbacks(*_args):
         error_callback.assert_called_once_with(456, a_batch_element_error)
 
     def test_when_called_with_mixed_array_lengths_then_IndexError_is_raised(
-        self, manager, some_refs, a_traitsdata, a_context
-    ):
-        datas = [a_traitsdata for _ in some_refs]
-
-        with pytest.raises(IndexError):
-            manager.register(some_refs[1:], datas, a_context, mock.Mock(), mock.Mock())
-
-        with pytest.raises(IndexError):
-            manager.register(some_refs, datas[1:], a_context, mock.Mock(), mock.Mock())
-
-    def test_when_called_with_varying_trait_sets_then_ValueError_is_raised(
-        self, manager, some_refs, a_context
+        self, manager, some_refs, some_entity_traitsdatas, a_context
     ):
-        datas = [TraitsData({f"trait{i}", "🦀"}) for i in range(len(some_refs))]
+        expected_message = (
+            "Parameter lists must be of the same length: {} entity references vs. {} traits datas."
+        )
+        with pytest.raises(
+            IndexError, match=expected_message.format(len(some_refs) - 1, len(some_refs))
+        ):
+            manager.register(
+                some_refs[1:], some_entity_traitsdatas, a_context, mock.Mock(), mock.Mock()
+            )
 
-        with pytest.raises(ValueError):
-            manager.register(some_refs, datas, a_context, mock.Mock(), mock.Mock())
+        with pytest.raises(
+            IndexError, match=expected_message.format(len(some_refs), len(some_refs) - 1)
+        ):
+            manager.register(
+                some_refs, some_entity_traitsdatas[1:], a_context, mock.Mock(), mock.Mock()
+            )
 
     def test_when_called_with_None_data_then_TypeError_is_raised(
         self, manager, some_refs, a_context, a_traitsdata
@@ -2948,17 +3025,12 @@ def an_empty_traitsdata():
 @pytest.fixture
 def some_entity_traitsdatas():
     first = TraitsData({"a_trait"})
-    second = TraitsData({"a_trait"})
+    second = TraitsData({"a_trait", "a_different_trait"})
     first.setTraitProperty("a_trait", "a_prop", 123)
     second.setTraitProperty("a_trait", "a_prop", 456)
     return [first, second]
 
 
-@pytest.fixture
-def some_populated_traitsdatas():
-    return [TraitsData(set("trait1")), TraitsData(set("trait2"))]
-
-
 @pytest.fixture
 def a_traitsdata():
     return TraitsData(set())