Skip to content

docs: add ADR for cross-instance taxonomy identity#656

Open
mgwozdz-unicon wants to merge 4 commits into
openedx:mainfrom
mgwozdz-unicon:cbe-cross-instance-taxonomy-identity-adr
Open

docs: add ADR for cross-instance taxonomy identity#656
mgwozdz-unicon wants to merge 4 commits into
openedx:mainfrom
mgwozdz-unicon:cbe-cross-instance-taxonomy-identity-adr

Conversation

@mgwozdz-unicon

Copy link
Copy Markdown
Contributor
  • Adds ADR 0011, proposing a stable, immutable uuid field on Taxonomy so that competency
    criteria copied along with course content (new course run, course export/import, course/library
    copy) can be bound by reference to the correct taxonomy on the target instance, rather than an
    export_id match that isn't reliable across instances.
  • Defines the reconciliation policy for repeat imports: if the incoming tag diff is additions
    only, apply it; if it includes any rename, reparent, or delete, refuse the import rather than
    risk silently mutating taxonomy state that unrelated content on the target depends on.
  • Scopes this decision to the legacy XBlock/modulestore course path only. Cross-instance library
    copy, the newer openedx_content Component/Container model, and full taxonomy/tag versioning
    are explicitly out of scope, noted as excluded or deferred with reasoning.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jul 10, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @mgwozdz-unicon!

This repository is currently maintained by @axim-engineering.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@bradenmacdonald bradenmacdonald left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not totally opposed to using UUIDs here, but I don't think it's necessary and I do think you can achieve everything that you want by making an effort to make the default value of export_id "more unique" than it currently is.

Comment on lines +22 to +24
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess it's not documented clearly but the goal of export_id is actually to identify taxonomies across different instances, exactly what you are requesting with this ADR. It is meant for answering "does the target already have this taxonomy."

See openedx/modular-learning#183

"As a content author, I want a consistent way to identify taxonomies, especially across instances".

Comment on lines +50 to +59
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a UUID is what we want.

If I set up the Lightcast Open Skills taxonomy on instance A, and someone else sets up the Lightcast Open Skills taxononomy on instance B, there should be some way to indicate that they're the same, and future exports/imports across the instances will handle that. But if they each get an immutable UUID at the time of original import, then there is no way to achieve that. Whereas with export ID, it's easy, you just make sure they both have the same export ID like io.lightcast.open-skills.

Comment on lines +166 to +169
``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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please link to #455 which is our tentative plan for making taxonomies versioned.

Comment on lines +174 to +184
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I was saying above, but it can be done more easily via export_id. It's awkward to compare and edit UUIDs.

Reuse the existing Taxonomy.export_id field for cross-instance
identity instead of adding a new uuid field, per review feedback.
@mgwozdz-unicon

Copy link
Copy Markdown
Contributor Author

Thanks @bradenmacdonald , this makes sense, went with export_id in the latest commit rather than adding a new uuid field.

You're right that export_id already has what's needed here: it's required, unique, and format-validated at the model level, and the REST API already accepts a caller-supplied value on create. Linked modular-learning#183 in the Context section so the intended purpose is documented where the next person will look for it.

Additionally:

  • Added "Add a new immutable uuid field" to Rejected Alternatives, so the reasoning for not going that route is on record.
  • The "Manual merge of independently-created taxonomies" deferred item now notes that today this requires a direct API call: Studio's Taxonomy Editing UI has no field to set or edit export_id.
  • Linked openedx-core#455 in the versioning section per your comment there.

Let me know if I've missed anything.

@ormsbee ormsbee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @bradenmacdonald's review, and using export_id. It's similar to what we already do for things like courses and libraries, where we rely on meaningful keys.

One thing I want flag is that courses and libraries are implicitly namespaced to an org, which is something that we're not currently doing with taxonomies. We don't really have system vs. org-level libraries or courses. If there is ever a situation in which multiple orgs want to independently tweak or update the same logical taxonomy, we may want to consider some kind of namespacing conventions.

@mgwozdz-unicon

Copy link
Copy Markdown
Contributor Author

One thing I want flag is that courses and libraries are implicitly namespaced to an org, which is something that we're not currently doing with taxonomies. We don't really have system vs. org-level libraries or courses. If there is ever a situation in which multiple orgs want to independently tweak or update the same logical taxonomy, we may want to consider some kind of namespacing conventions.

I added a note about this to the "Excluded from this decision" section to flag it for the future.

@mgwozdz-unicon mgwozdz-unicon requested a review from ormsbee July 13, 2026 12:48
@mphilbrick211 mphilbrick211 added the FC Relates to an Axim Funded Contribution project label Jul 13, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Ready for Review in Contributions Jul 13, 2026

@jesperhodge jesperhodge left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some clarifications needed :)

Context
-------

Competency Based Education (CBE) will bring about the use case where, when course content is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this ADR uses "competency criteria," can we get one plain sentence defining it? Something like: "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." Right now CompetencyCriteria/CompetencyCriteriaGroup show up in parentheses on line 17 with zero setup, and this ADR lives in openedx_tagging, which explicitly isn't supposed to know about competencies. A reader coming from the tagging side needs to know why a criterion touches a taxonomy at all before the rest of the Context makes sense.

Comment on lines +17 to +20
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
it. Each competency criterion (CompetencyCriteria) identifies the competency it evaluates by referencing a specific Tag in a Taxonomy. Criteria themselves are versioned via django-simple-history (see :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 land somewhere with a matching tag for it to reference.

Copy semantics
~~~~~~~~~~~~~~

Competency criteria are copied **by reference**: the target's criteria are bound to a taxonomy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually work?

  • A CompetencyCriteria is indeed bound to a tag application that is bound to an object and a taxonomy, where the object is an object in e.g. the course that's being copied. Does a course copy copy the taxonomy, and do the tags resolve so that the object tag ids actually apply to the new taxonomy_id that has the same export_id?
  • A CompetencyCriteria is also bound to a CompetencyCriteriaGroup. How is this CompetencyCriteriaGroup copied to the new course or content?
  • Do I understand by reference to mean a reference of the criteria to the export_id of a taxonomy? Or by reference to the competency criteria? In that case I'm confused who references them and how the reference could be resolved if we have a different environment with a different database.
  • A CompetencyCriteriaGroup is optionally bound to a course_id. I assume that is a course run. I assume it actually has to have the course_id to apply to a course. Since a CompetencyCriteria belongs to a CompetencyCriteriaGroup that belongs to a course, wouldn't copying it by reference mean that it does not apply to the new course now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify my point 1:
An object_tag encodes a course run in the object_id. So if we do a course re-run, for example, the object_tags reference the previous course run, which means that the new course has no tags. When tags are copied to the new course, the copy of the tags somehow needs to be resolved to the new object_ids of the course content, not the same as previously. I assume this is handled via this tags file in course imports - and we will now apply the same logic to re-runs and such - but can you confirm this assumption is true?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is handled at all in course re-runs today. (But it should be.) Some testing will be required.

@jesperhodge

Copy link
Copy Markdown
Contributor

Claude flagged this:
"One inconsistency worth flagging for the team: ADR 0002 says course_id FKs to openedx_catalog_courserun, while ADR 0011 frames the near-term copy target as the legacy modulestore course. The two ADRs describe different course-model layers without reconciling them."

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.
@mgwozdz-unicon

Copy link
Copy Markdown
Contributor Author

@jesperhodge, addressed your comments:

  1. Added a plain-sentence definition of a competency criterion to the Context section, and pointed to ADR 0002 for the full model. This ADR still only owns taxonomy/tag identity, not the competency model itself.
  2. Took your suggested wording for that paragraph, with minor tweaks.
  3. This was a fair question, "by reference" was doing double duty for two different things. Now split apart in Copy semantics:
    • Taxonomy/Tag are never duplicated, for any of the three mechanisms. That's the only thing "by reference" describes now: same-instance mechanisms (new course run, library copy) already share the identical taxonomy row, so there's nothing to resolve; cross-instance (course export/import) resolves the reference via export_id, per the existing Resolution on import section.
    • ObjectTag, CompetencyCriteria, and CompetencyCriteriaGroup are copied by value: duplicated and re-keyed to the new object/course ids, using an old-id-to-new-id mapping built during the copy. A new CompetencyCriteriaGroup shares a parent group with the original (combined by OR by default).
    • So to your bullet 4: yes, a CompetencyCriteriaGroup copied this way does end up with the new course_id, since it's a new row, not the old one repointed.
  4. Confirmed by checking the actual rerun code: ObjectTag copying is not implemented for course reruns today, only for unrelated flows (block duplicate/paste, library-to-course upstream sync). The Context section now says this plainly rather than implying copy_tags() just needs wiring in. Building this is new platform work; agreed it's a scope conversation for the team rather than something to spell out in the ADR.

On the course_id inconsistency you flagged separately: checked both ADRs against the actual CourseRun model. It's not actually two competing identifier systems, CourseRun.course_key (openedx_catalog) is the same modulestore course key ADR 0011 already talks about; the model's docstring says it's meant to be the canonical FK target for exactly this kind of reference. ADR 0011 just never said so. Added a sentence in Context tying "course" explicitly to CourseRun, so it's clear this is the same course-run identity ADR 0002's course_id references, not a different one.

@bradenmacdonald your suspicion on the rerun/ObjectTag question was right, confirmed above.

@bradenmacdonald

Copy link
Copy Markdown
Contributor

@mgwozdz-unicon @jesperhodge Note that there is code to duplicate tags when an XBlock is duplicated but reruns don't involve the XBLOCK_DUPLICATED event.

We probably need to add a similar handler for the COURSE_RERUN_COMPLETED event.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U

Projects

Status: Ready for Review

Development

Successfully merging this pull request may close these issues.

6 participants