From 10704bd309ec99a6edb854ea3ddf3c973e59e054 Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Thu, 9 Jul 2026 17:29:12 -0700 Subject: [PATCH 1/4] docs: add ADR for cross-instance taxonomy identity --- .../0011-cross-instance-taxonomy-identity.rst | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst diff --git a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst new file mode 100644 index 000000000..ce6280520 --- /dev/null +++ b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst @@ -0,0 +1,191 @@ +.. _openedx-tagging-adr-0011: + +11. Cross-instance identity for taxonomies copied with course content +======================================================================== + +Status +------ + +Proposed + +Context +------- + +Competency Based Education (CBE) will bring about the use case where, when course content is +copied by any of the three supported mechanisms, a new course run, course export/import, or +course/library copy, any competency criteria attached to that content should be copied along with +it. Competency criteria (``CompetencyCriteria``, ``CompetencyCriteriaGroup``) are versioned via +``django-simple-history`` per :ref:`openedx-learning-adr-0003`, but the taxonomies and tags they +reference are deliberately non-evaluative, unversioned display metadata. Copying a criterion +therefore also means resolving the taxonomy and tags it points to on the target. + +No stable, cross-instance identity exists for a ``Taxonomy`` or ``Tag`` today. ``Taxonomy.export_id`` +and ``Tag.external_id`` are both editable, instance-scoped identifiers designed for import-file +bookkeeping (see :ref:`openedx-tagging-adr-0006`), not for answering "does the target already have +this taxonomy." + +The three copy mechanisms differ significantly in current maturity: + +- **Course export/import** (legacy XBlock/modulestore course): already works, via a ``tags.csv`` + sibling file resolved by ``export_id`` at import time. +- **New course run**: always same-instance/same-database. ``copy_tags()`` exists in + ``openedx_tagging.api`` but is not wired into the course-rerun flow. +- **Library copy**: always same-instance today; no cross-instance transport exists in + ``content_libraries``, and none is planned. + +Excluded from this decision: + +- Cross-instance library copy. Not a near-term platform capability; out of scope. +- The newer ``openedx_content`` Component/Container content model. It has no tag-copy wiring at + all today; this decision targets the legacy course model that currently carries course content, + and a future extension should reuse the identity contract defined here rather than re-deriving it. +- Versioning ``Taxonomy``/``Tag`` themselves (see Deferred, below). + +Decision +-------- + +Stable identity +~~~~~~~~~~~~~~~~ + +Add an immutable ``uuid`` field to ``Taxonomy``, generated at creation and preserved through +export/import. ``export_id`` cannot serve this purpose on its own: it is free text, chosen and +editable by whoever administers a taxonomy, so nothing guarantees that the same ``export_id`` on +two different instances refers to the same taxonomy, or that two different taxonomies on two +instances never happen to share one. A ``uuid`` is generated once, never touched by a person, and +so cannot collide or drift the way a human-chosen identifier can. ``export_id`` keeps its existing +role as the human-meaningful identifier; ``uuid`` adds the machine identity it was never designed +to provide. This follows the existing convention in this codebase of using ``uuid`` for a stable +external reference (see ``PublishableEntity.uuid``, :ref:`openedx-content-adr-0003`), applied here +to a model that currently lacks it. + +``Tag`` does not need a new identifier: ``Tag.external_id`` (already used by the tag import/export +plan-building logic, see :ref:`openedx-tagging-adr-0006`) is sufficient for within-taxonomy +matching. Free-text taxonomies have no ``Tag`` rows and travel as literal strings unconditionally; +no reconciliation applies to them. + +Copy semantics +~~~~~~~~~~~~~~ + +Competency criteria are copied **by reference**: the target's criteria are bound to a taxonomy +sharing the source's ``uuid``, not to an independent duplicate. This is a larger commitment than a +by-value copy, but a by-value copy would leave the target's competency evaluation permanently +disconnected from the taxonomy it depends on, undermining the goal of this use case. + +Resolution on import +~~~~~~~~~~~~~~~~~~~~~ + +On import, whether the source and target are the same deployment or two different organizations' +instances, the behavior is uniform: + +- If no taxonomy with a matching ``uuid`` exists on the target, auto-create one, seeded from the + tags that traveled with the export. +- If a taxonomy with a matching ``uuid`` already exists, reconcile it (see below) rather than + creating a duplicate. + +A single uniform rule was chosen over branching by deployment relationship because the +reconciliation policy below already guards against silent corruption in both cases; adding a +second behavior for the cross-organization case would add complexity without removing risk. + +Reconciliation on repeat import +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a matching taxonomy already exists on the target, build a plan comparing the incoming tag +snapshot against the target's current tags, reusing the existing tag import/export plan-building +logic (``TagImportPlan``, keyed on ``Tag.external_id``) that already classifies differences into +create/rename/reparent/delete actions. + +- If every action in the plan is a tag creation, apply it: the target gains the new tags, nothing + existing is touched. +- If the plan contains any rename, reparent, or delete action, refuse the import of criteria bound + to that taxonomy. + +The actor triggering a course copy has no standing to mutate a taxonomy shared with other, +unrelated content on the target instance. In a manual tag re-import (the existing use of this +plan-building logic), the file represents the deliberate intent of whoever owns that taxonomy. A +copied course's snapshot only reflects what the source looked like at export time: a tag missing +from it doesn't mean the source deleted it, it may be something the target added independently. +Auto-applying deletes or renames on that basis risks silently corrupting taxonomy state that +unrelated courses on the target depend on. Additions carry no such risk: they only ever add new +identities, never touch existing ones. + +Failure surfacing +~~~~~~~~~~~~~~~~~~ + +A refusal is modeled as an ordinary import task failure, using the existing +``UserTaskStatus.fail(message)`` / ``Error`` artifact / ``import_status_handler`` mechanism in +``contentstore``. The Studio Authoring MFE's import flow already reads this same ``Message`` field +(``CourseImportContext.tsx``), so this requires no new frontend or backend surface, and no +distinction needs to be drawn between a Platform Administrator and a Course Author watching the +same import: whoever is watching sees the existing failure message. + +Alternatives Considered +------------------------ + +Copy by value (independent duplicate) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Give the target its own disconnected copy of the taxonomy and tags, with no ongoing identity link +to the source. + +**Pros:** cheapest option; no cross-instance identity needed at all. + +**Cons:** breaks the moment the source taxonomy changes; the target's criteria would reference a +frozen, immediately stale copy. Not chosen because it does not meet the intent of this use case as +well as by-reference does. + +Branch no-match behavior by deployment relationship +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Auto-create a stub taxonomy only for same-deployment imports; require manual administrator +reconciliation for a different organization's instance. + +**Pros:** more conservative for the genuinely untrusted case. + +**Cons:** two behaviors to build, test, and document instead of one; the additions-only +reconciliation policy already protects against silent corruption regardless of relationship, +making the extra branch unnecessary complexity. + +Deferred +-------- + +These are known gaps in the design above, intentionally left unaddressed for now. None of them are +precluded by this decision; each could be added later without revisiting the identity model above. + +Fancier reconciliation for non-additive diffs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The design above refuses any import whose diff includes a rename, reparent, or delete, rather than +attempting to apply or merge it. A more capable system could instead present the conflicting +changes for guided reconciliation rather than a flat refusal. Not built now because a flat refusal +is enough to satisfy this use case safely; worth revisiting if refusals turn out to be common in +practice. + +Real versioning for taxonomies and tags +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Taxonomy``/``Tag`` could gain version history (e.g. via ``django-simple-history``, matching +``CompetencyCriteria``), which would let drift between source and target be tracked precisely +rather than detected only as additive or not. Likely needed eventually, but not required for this +use case, since the additions-only reconciliation policy already satisfies it without history. + +Manual merge of independently-created taxonomies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two institutions may independently author what they each consider the same taxonomy on their own +instances, without ever having copied content between each other. Since a ``uuid`` establishes +identity only once two taxonomies have actually shared a copy operation, importing between them +today would create a second, unrelated taxonomy on the receiving side, not recognize them as the +same one. Automatic matching cannot safely resolve this: falling back to matching by name or +content similarity would reintroduce the false-positive risk ``uuid`` was introduced to avoid, two +instances with genuinely different taxonomies that happen to look similar would be silently +merged. The eventual resolution is a deliberate, human-initiated action: an administrator manually +reassigns one instance's taxonomy to adopt the other's ``uuid``, retroactively establishing shared +identity going forward. Not designed here, since it is a distinct, rare operation, orthogonal to +the copy-time behavior this decision covers. + +Changelog +--------- + +2026-07-09: + +* Initial draft. From 71830a8f103c2d40e1796325abc687f5f5663fa0 Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Fri, 10 Jul 2026 16:34:39 -0700 Subject: [PATCH 2/4] docs: use export_id for cross-instance taxonomy identity Reuse the existing Taxonomy.export_id field for cross-instance identity instead of adding a new uuid field, per review feedback. --- .../0011-cross-instance-taxonomy-identity.rst | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst index ce6280520..8b42520f8 100644 --- a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst +++ b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst @@ -19,10 +19,12 @@ it. Competency criteria (``CompetencyCriteria``, ``CompetencyCriteriaGroup``) ar reference are deliberately non-evaluative, unversioned display metadata. Copying a criterion therefore also means resolving the taxonomy and tags it points to on the target. -No stable, cross-instance identity exists for a ``Taxonomy`` or ``Tag`` today. ``Taxonomy.export_id`` -and ``Tag.external_id`` are both editable, instance-scoped identifiers designed for import-file -bookkeeping (see :ref:`openedx-tagging-adr-0006`), not for answering "does the target already have -this taxonomy." +No stable, cross-instance identity exists for a ``Tag`` today: ``Tag.external_id`` is an editable, +instance-scoped identifier designed for import-file bookkeeping (see :ref:`openedx-tagging-adr-0006`). +``Taxonomy.export_id`` is different: per `modular-learning#183 +`_, it was always intended to answer "does +the target already have this taxonomy," which is exactly what this decision needs; it just hasn't +been documented as such, or exercised for identity-matching purposes, until now. The three copy mechanisms differ significantly in current maturity: @@ -47,16 +49,18 @@ Decision Stable identity ~~~~~~~~~~~~~~~~ -Add an immutable ``uuid`` field to ``Taxonomy``, generated at creation and preserved through -export/import. ``export_id`` cannot serve this purpose on its own: it is free text, chosen and -editable by whoever administers a taxonomy, so nothing guarantees that the same ``export_id`` on -two different instances refers to the same taxonomy, or that two different taxonomies on two -instances never happen to share one. A ``uuid`` is generated once, never touched by a person, and -so cannot collide or drift the way a human-chosen identifier can. ``export_id`` keeps its existing -role as the human-meaningful identifier; ``uuid`` adds the machine identity it was never designed -to provide. This follows the existing convention in this codebase of using ``uuid`` for a stable -external reference (see ``PublishableEntity.uuid``, :ref:`openedx-content-adr-0003`), applied here -to a model that currently lacks it. +Use the existing ``Taxonomy.export_id`` field as the cross-instance identity, rather than adding a +new field. ``export_id`` is already required, unique, and format-validated +(``^[\w\-.]+$``) at the model level, and the REST API already accepts a caller-supplied value on +taxonomy creation. Two institutions that each set up the same third-party taxonomy (for example, +Lightcast Open Skills) can establish that they're the same taxonomy simply by using the same +``export_id`` (for example, a reverse-DNS-style value like ``io.lightcast.open-skills``), something +an immutable, randomly-generated identifier could never let them do, since two independent imports +would always get two different random values with no way to reconcile them afterward. + +``export_id``'s existing mutability (it can be edited after creation) is also a feature here rather +than a gap: it is how the deferred manual-merge case below would actually be performed, by editing +one instance's ``export_id`` to match the other's. ``Tag`` does not need a new identifier: ``Tag.external_id`` (already used by the tag import/export plan-building logic, see :ref:`openedx-tagging-adr-0006`) is sufficient for within-taxonomy @@ -67,7 +71,7 @@ Copy semantics ~~~~~~~~~~~~~~ Competency criteria are copied **by reference**: the target's criteria are bound to a taxonomy -sharing the source's ``uuid``, not to an independent duplicate. This is a larger commitment than a +sharing the source's ``export_id``, not to an independent duplicate. This is a larger commitment than a by-value copy, but a by-value copy would leave the target's competency evaluation permanently disconnected from the taxonomy it depends on, undermining the goal of this use case. @@ -77,9 +81,9 @@ Resolution on import On import, whether the source and target are the same deployment or two different organizations' instances, the behavior is uniform: -- If no taxonomy with a matching ``uuid`` exists on the target, auto-create one, seeded from the +- If no taxonomy with a matching ``export_id`` exists on the target, auto-create one, seeded from the tags that traveled with the export. -- If a taxonomy with a matching ``uuid`` already exists, reconcile it (see below) rather than +- If a taxonomy with a matching ``export_id`` already exists, reconcile it (see below) rather than creating a duplicate. A single uniform rule was chosen over branching by deployment relationship because the @@ -121,6 +125,23 @@ same import: whoever is watching sees the existing failure message. Alternatives Considered ------------------------ +Add a new immutable ``uuid`` field +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add a new, randomly-generated ``uuid`` field on ``Taxonomy``, instead of reusing ``export_id``. + +**Pros:** guaranteed collision-free by construction; no reliance on a human choosing a consistent +value. + +**Cons:** a randomly-generated identifier can never let two independently-created taxonomies on +two different instances establish that they're the same one, exactly the "manual merge" case +deferred below, since two independent imports always produce two different random values with no +way to reconcile them. ``export_id`` already exists for this purpose (`modular-learning#183 +`_), is already required, unique, and +format-validated, and its editability is what makes the deferred manual-merge case possible at +all. Not chosen, since it would have duplicated ``export_id``'s role while being strictly less +capable for the independently-created-taxonomies case. + Copy by value (independent duplicate) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -166,22 +187,25 @@ Real versioning for taxonomies and tags ``Taxonomy``/``Tag`` could gain version history (e.g. via ``django-simple-history``, matching ``CompetencyCriteria``), which would let drift between source and target be tracked precisely rather than detected only as additive or not. Likely needed eventually, but not required for this -use case, since the additions-only reconciliation policy already satisfies it without history. +use case, since the additions-only reconciliation policy already satisfies it without history. See +`openedx-core#455 `_ for the tentative plan for +taxonomy versioning. Manual merge of independently-created taxonomies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Two institutions may independently author what they each consider the same taxonomy on their own -instances, without ever having copied content between each other. Since a ``uuid`` establishes -identity only once two taxonomies have actually shared a copy operation, importing between them -today would create a second, unrelated taxonomy on the receiving side, not recognize them as the -same one. Automatic matching cannot safely resolve this: falling back to matching by name or -content similarity would reintroduce the false-positive risk ``uuid`` was introduced to avoid, two -instances with genuinely different taxonomies that happen to look similar would be silently -merged. The eventual resolution is a deliberate, human-initiated action: an administrator manually -reassigns one instance's taxonomy to adopt the other's ``uuid``, retroactively establishing shared -identity going forward. Not designed here, since it is a distinct, rare operation, orthogonal to -the copy-time behavior this decision covers. +instances, without ever having copied content between each other. If they haven't both deliberately +set the same ``export_id``, importing between them today would create a second, unrelated taxonomy +on the receiving side, not recognize them as the same one. Automatic matching cannot safely resolve +this: falling back to matching by name or content similarity would reintroduce the false-positive +risk a deliberate, unique identifier was introduced to avoid, two instances with genuinely different +taxonomies that happen to look similar would be silently merged. The eventual resolution is a +deliberate, human-initiated action: an administrator manually edits one instance's taxonomy to +adopt the other's ``export_id``, retroactively establishing shared identity going forward. Not +designed here, since it is a distinct, rare operation, orthogonal to the copy-time behavior this +decision covers. Today this requires a direct API call: Studio's Taxonomy Editing UI has no field +to set or edit ``export_id``. Changelog --------- From a781ec45be52445ce4fe137440671435324c3494 Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Mon, 13 Jul 2026 06:47:00 -0600 Subject: [PATCH 3/4] docs: note org-level namespacing as excluded from taxonomy identity ADR --- .../decisions/0011-cross-instance-taxonomy-identity.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst index 8b42520f8..75c56870b 100644 --- a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst +++ b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst @@ -42,6 +42,10 @@ Excluded from this decision: all today; this decision targets the legacy course model that currently carries course content, and a future extension should reuse the identity contract defined here rather than re-deriving it. - Versioning ``Taxonomy``/``Tag`` themselves (see Deferred, below). +- Org-level namespacing for taxonomies. Unlike courses/libraries, taxonomies aren't scoped to an + org today; if multiple orgs ever need to independently evolve what they consider "the same" + taxonomy, some namespacing convention may be needed. Not addressed here since no such conflict + exists yet. Decision -------- From 61381cb82a5e69cdef57f5082b36b341ec5afd0f Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Wed, 15 Jul 2026 14:21:36 -0600 Subject: [PATCH 4/4] docs: clarify copy semantics vs taxonomy identity in ADR 0011 Distinguishes CompetencyCriteria/CompetencyCriteriaGroup/ObjectTag recreation (copied by value, re-keyed to new object/course ids) from Taxonomy/Tag identity (never duplicated, resolved by reference via export_id). Also defines competency criterion in plain terms with a reference to ADR 0002, and ties "course" to CourseRun (openedx_catalog), reconciling with ADR 0002's course_id. --- .../0011-cross-instance-taxonomy-identity.rst | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst index 75c56870b..0da50de06 100644 --- a/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst +++ b/docs/openedx_tagging/decisions/0011-cross-instance-taxonomy-identity.rst @@ -11,13 +11,23 @@ Proposed Context ------- -Competency Based Education (CBE) will bring about the use case where, when course content is -copied by any of the three supported mechanisms, a new course run, course export/import, or -course/library copy, any competency criteria attached to that content should be copied along with -it. Competency criteria (``CompetencyCriteria``, ``CompetencyCriteriaGroup``) are versioned via -``django-simple-history`` per :ref:`openedx-learning-adr-0003`, but the taxonomies and tags they -reference are deliberately non-evaluative, unversioned display metadata. Copying a criterion -therefore also means resolving the taxonomy and tags it points to on the target. +Competency Based Education (CBE) introduces ``CompetencyCriteria``: see +:ref:`openedx-learning-adr-0002` for the full model, but in brief, a competency criterion asserts +that a piece of course content demonstrates a specific competency, which it identifies by pointing +at a Tag in a Taxonomy. Criteria are grouped into a ``CompetencyCriteriaGroup`` tree, also defined +in ADR 0002, that combines them with AND/OR logic and may be scoped to a specific course run +(``CourseRun`` in ``openedx_catalog``, identified by the same ``course_key`` that identifies the +corresponding legacy modulestore course; "course" throughout this decision means that same +course-run identity, the one ADR 0002's ``course_id`` references). + +When course content is copied by any of the three supported mechanisms, a new course run, course +export/import, or course/library copy, any competency criteria attached to that content should be +copied along with it. Criteria themselves are versioned via ``django-simple-history`` per +:ref:`openedx-learning-adr-0003`, but the taxonomy and tags they reference are deliberately not: +they're non-evaluative display metadata that can change independently without creating a new +version of the criteria pointing at them. Copying a criterion therefore also means resolving the +taxonomy and tags it points to on the target, since the copy has to have a corresponding tag for it +to reference. No stable, cross-instance identity exists for a ``Tag`` today: ``Tag.external_id`` is an editable, instance-scoped identifier designed for import-file bookkeeping (see :ref:`openedx-tagging-adr-0006`). @@ -30,8 +40,10 @@ The three copy mechanisms differ significantly in current maturity: - **Course export/import** (legacy XBlock/modulestore course): already works, via a ``tags.csv`` sibling file resolved by ``export_id`` at import time. -- **New course run**: always same-instance/same-database. ``copy_tags()`` exists in - ``openedx_tagging.api`` but is not wired into the course-rerun flow. +- **New course run**: always same-instance/same-database, so no taxonomy/tag resolution is ever + needed; what's missing is recreating the ``ObjectTag``, ``CompetencyCriteria``, and + ``CompetencyCriteriaGroup`` rows for the new run (see Copy semantics, below). ``copy_tags()`` + exists in ``openedx_tagging.api`` but is not wired into the course-rerun flow yet. - **Library copy**: always same-instance today; no cross-instance transport exists in ``content_libraries``, and none is planned. @@ -74,14 +86,30 @@ no reconciliation applies to them. Copy semantics ~~~~~~~~~~~~~~ -Competency criteria are copied **by reference**: the target's criteria are bound to a taxonomy -sharing the source's ``export_id``, not to an independent duplicate. This is a larger commitment than a -by-value copy, but a by-value copy would leave the target's competency evaluation permanently -disconnected from the taxonomy it depends on, undermining the goal of this use case. +Competency criteria are recreated on the target, not moved. For any of the three mechanisms, the +``ObjectTag``, ``CompetencyCriteria``, and ``CompetencyCriteriaGroup`` rows attached to the copied +content are duplicated and re-keyed to the new object and course ids, using an old-id-to-new-id +mapping built during the copy. A recreated ``CompetencyCriteriaGroup`` shares a parent group with +the original, so the two are combined by ``OR`` by default. + +The ``Taxonomy``/``Tag`` rows a recreated ``ObjectTag`` points to are handled differently: they are +never duplicated, only referenced, and it's this taxonomy/tag relationship the rest of this +decision means by **by reference**. For a same-instance mechanism (new course run, library copy), +source and target already share the identical taxonomy row, so no resolution is needed at all. For +a cross-instance mechanism (course export/import), the reference is resolved to whichever taxonomy +on the target shares the source's ``export_id``, per Resolution on import, below. This is a larger +commitment than a by-value taxonomy copy, but a by-value copy would leave the target's competency +evaluation permanently disconnected from the taxonomy it depends on, undermining the goal of this +use case. Resolution on import ~~~~~~~~~~~~~~~~~~~~~ +This section, and Reconciliation on repeat import, below, apply specifically to course +export/import: it is the only mechanism where a matching taxonomy might not already exist on the +target. New course run and library copy need no taxonomy-side resolution at all, per Copy +semantics, above. + On import, whether the source and target are the same deployment or two different organizations' instances, the behavior is uniform: