Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Decision

Lifecycle rules for this parent/child pair:

- Creating a competency taxonomy creates both the parent ``oel_tagging_taxonomy`` row and the ``CompetencyTaxonomy`` row in one transaction.
- Creating a competency taxonomy creates both the parent ``oel_tagging_taxonomy`` row and the ``CompetencyTaxonomy`` row in one transaction, and also sets that ``oel_tagging_taxonomy`` row's ``taxonomy_type`` to ``competency`` in the same transaction (``openedx_tagging`` ADR 0013).
- Deleting either representation is treated as deleting the competency taxonomy and removes both rows, subject to Decision 7 delete protections.

2. ``CompetencyCriteriaGroup`` concept (database table)
Expand Down Expand Up @@ -366,6 +366,12 @@ Rejected Alternatives
4. Makes it harder to keep competency features optional for deployments that only want generic tagging
5. Increases risk of future refactor/migration work if the competency domain later needs to be split from tagging

This rejection stands, unaffected by whether ``CompetencyTaxonomy`` currently defines
any columns of its own beyond the parent link. ``openedx_tagging`` ADR 0013 separately
adds a ``taxonomy_type`` field to ``oel_tagging_taxonomy`` for a narrower, later problem
this ADR doesn't cover: a lightweight type label for ``openedx_tagging``'s own generic
REST API. See that ADR and the Changelog below.

2. Same as above except combine the ``CompetencyCriteria`` and ``oel_tagging_objecttag`` tables by adding the rule information as columns on the ``oel_tagging_objecttag`` table. This would be a more denormalized approach that would reduce the number of joins needed to retrieve competency achievement criteria information but would add complexity to the ``oel_tagging_objecttag`` table and make it less flexible for other uses.

1. Pros
Expand Down Expand Up @@ -422,3 +428,14 @@ Rejected Alternatives

1. Silently does not work on this project's tested and production database backend. Django compiles a conditional ``UniqueConstraint`` to a partial index, which MySQL does not support; Django raises only a non-fatal system-check warning (``models.W036``) and skips creating the constraint, leaving the uniqueness rule completely unenforced at the database level.
2. The gap would surface only as a data-integrity incident under concurrent writes, not as a test or migration failure, since SQLite (used for quick local test runs) does support partial indexes and would mask the problem in that environment.

Changelog
---------

2026-07-16:

* Added the ``taxonomy_type`` creation-time invariant to Decision 1's lifecycle rules, and
a note on Rejected Alternatives item 1 that its rejection stands independent of
``openedx_tagging`` ADR 0013, which separately adds a ``taxonomy_type`` field to
``oel_tagging_taxonomy`` for a narrower problem (a type label for that library's generic
REST API, #618) than this ADR's data-model decision.
132 changes: 132 additions & 0 deletions docs/openedx_tagging/decisions/0013-taxonomy-type-field.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
.. _openedx-tagging-adr-0013:

13. Base-model ``taxonomy_type`` field for Competency Taxonomies
==================================================================

Status
------

Proposed

Context
-------

The taxonomy Get endpoints (``TaxonomySerializer``) need to report whether a taxonomy is
a Competency Taxonomy, so that Studio can badge Competency Taxonomies and gate access to
the Competency Management page (#618), symmetrically with the Create/Import endpoint's
``taxonomy_type`` field (#614). ``CompetencyTaxonomy`` is a Django multi-table-inheritance
subclass of ``Taxonomy`` (``CompetencyTaxonomy(Taxonomy)``), owned by the CBE applet and
defined in `ADR 0002 <../../openedx_learning/decisions/0002-competency-criteria-model.rst>`_.

``openedx_tagging`` is a generic tagging library with no knowledge of any specific taxonomy
flavor built on top of it, CBE or otherwise, and must not gain any: a specific downstream
applet's model or relation name has no business appearing in this library's source, since
that reverses the intended dependency direction (CBE depends on ``openedx_tagging``, not
the other way around) and would break for any Open edX install running ``openedx_tagging``
without CBE installed.

Two approaches were tried on #618 and found unsound; see Rejected Alternatives.

This decision does not reopen `ADR 0002 <../../openedx_learning/decisions/0002-competency-criteria-model.rst>`_'s
rejection of a flat ``taxonomy_type`` column as an alternative to multi-table inheritance.
That rejection's deciding reason was that a flat column can't give other CBE tables a
real, database-enforced foreign key to specifically a competency-enabled taxonomy;
``CompetencyRuleProfile.competency_taxonomy_id`` relies on that guarantee. That reasoning
is independent of whether ``CompetencyTaxonomy`` currently defines any columns of its own
beyond the parent link, and is unaffected by this decision. This decision addresses a
narrower, later problem ADR 0002 does not cover: a lightweight type label for
``openedx_tagging``'s own generic REST API.

Decision
--------

Add a ``taxonomy_type`` field directly to ``Taxonomy``:

- A ``TaxonomyType(models.TextChoices)`` enum with values ``TAGS`` (default) and
``COMPETENCY``, shared between this field and #614's write-side ``ChoiceField`` so the
two can never drift apart.
- Set explicitly by whichever code creates the row. The code that creates a
``CompetencyTaxonomy`` row (#614) also sets ``taxonomy_type=TaxonomyType.COMPETENCY`` on
the same ``Taxonomy`` row, in the same transaction that creates both rows (the existing
lifecycle rule in ADR 0002 Decision 1). ``openedx_tagging`` never inspects, imports, or
names ``CompetencyTaxonomy`` or any relation to it anywhere in its own source; the field's
value is opaque to it and owned entirely by the caller.
- Immutable after creation, mirroring the scope-immutability precedent for
``CompetencyRuleProfile`` in ADR 0002 Decision 3: a taxonomy's type does not change over
its lifetime.
- Existing taxonomies are backfilled to ``TaxonomyType.TAGS`` via an additive migration;
none has a ``CompetencyTaxonomy`` row today, since CBE has no production data yet.
- Exposed read-only on ``TaxonomySerializer`` (#618), returning the field's value directly.

**Known tradeoff.** This creates two facts that must stay in sync: the field's value, and
whether a ``CompetencyTaxonomy`` row actually exists for that taxonomy. This is accepted
because it is enforced at a single transactional chokepoint (the creation path for a
``CompetencyTaxonomy``), not as an ongoing invariant that application code must maintain
across multiple call sites.

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 sure about this.

Add a taxonomy_type field directly to Taxonomy: A TaxonomyType(models.TextChoices) enum with values TAGS (default) and COMPETENCY

So we're still hard-coding a reference to COMPETENCY in the tagging app, and we now have to ensure that we keep the field's value in sync with whether or not a CompetencyTaxonomy actually exists.

Plus, per the ADR 614, we have to update a whole bunch of other tagging REST APIs to support the taxonomy_type field.


If we're going to hard-code some reference to Competencies in the tagging app, I think what I'm leaning toward is something radically simpler:

Put this in the taxonomy list/get serializer, and that's it - no other changes needed to the taxonomy app.

# Technically this is a layering violation as our tagging/taxonomy app is not supposed
# to be aware of competencies, but it simplifies a lot for us and it doesn't require
# importing any competency code.
# We can revisit this in the future if we introduce other taxonomy types.
taxonomy_type = "competency" if hasattr(instance, "competencytaxonomy") else "tags"

REST APIs: I don't think there's any need to change the tagging REST APIs. To create a competency taxonomy from the frontend, you could either (A) call the "create taxonomy" REST API and on succes, call the "create competency metadata for taxonomy" REST API (from the cbe app), or (B) call a competency REST API that creates the Taxonomy and the CompetencyTaxonomy object together in a single request.

Export: You could add the taxonomy_type field to the JSON export data if that seems useful. It's not possible to include on CSVs.

Import: If the user chooses to import something as a Competency taxonomy (in the frontend UI), it's the same thing: import the taxonomy then call a second API to convert it to Competency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Going through in order:

On the sync-drift point: yes, I agree that this tradeoff is the risk, but hopefully it can be resolved at implementation time.

On the REST API surface per #614: I don't think ADR 13 is adding work there. Those endpoints need to accept taxonomy_type on create/import regardless of how the Get side reports it, so that's #614's scope either way, not something this ADR introduces on top.

On the hasattr suggestion: I hear the appeal, it's less code today, and I get that you're framing it as a deliberate, called-out layering violation rather than an accidental one. Thinking it through more, I don't think either approach avoids touching openedx_tagging when a new type shows up eventually, the field approach still means adding a value to the shared enum. Where I think they differ is what that touch requires: with the field, adding a type is adding a string to a catalog, the read path (TaxonomySerializer) doesn't change, it already just returns whatever's stored. With hasattr, adding a type means adding another branch to the serializer's logic that names a specific downstream app's MTI relation, so the read path itself keeps growing a fact about another app's model internals with every flavor. That feels like the thing the ADR is trying to keep out of a generic library, holding the type as a piece of data the caller owns, versus holding it as code that inspects other apps' structure. Also worth flagging: this looks close to option 2 from your comment on #618 (Adding a tiny bit of Competency awareness to the base tagging app), where you said you leaned toward 1+3 over it. Did something change your thinking there, or is this meant as a variant of option 1 rather than 2?

On separate Competency REST APIs: Since the Taxonomies landing page is meant to serve both flows, I don't think it makes sense to make the frontend juggle two calls, especially when this could become even more calls than that if the number of taxonomy types increases.

On export/import: agreed, that's already settled in #614 and not really in scope here.

@bradenmacdonald bradenmacdonald Jul 17, 2026

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.

Yeah, sorry, I'm still not happy with any of the approaches here so I'm just throwing out ideas to see if there's anything that you like. I'm not so unhappy that I'm going to block anything though. I'm reluctantly fine with any of these approaches, but I want to see if we can come up with something clean that doesn't have any layering violations.

Thinking about this more, what if we kept the taxonomy code in openedx-core / oel_tagging completely free of competency awareness, but use the platform's override of the REST API at openedx/core/djangoapps/content_tagging/rest_api/v1/ to inject the competency pieces you need in the REST API? Then you could add taxonomy_type = "competency" if hasattr(instance, "competencytaxonomy") else "tags" to the serializer and also wire through the "competency creation" changes to the REST API specified in #614 while still keeping some semblance of layer separation, because the core tagging API is unchanged.

So the layers would be:

  • oel_tagging python API & REST APIs in this repo - pure tagging/taxonomy only ✅
  • content_tagging python API in openedx-platform - pure tagging/taxonomy only ✅ (as specified on [BE] Update taxonomy Create & Import endpoints to accept Taxonomy Type #614 , "Layering constraint. openedx_tagging/api.py::create_taxonomy() creates only a plain Taxonomy. It must not import or call anything from openedx_learning. The CompetencyTaxonomy creation belongs exclusively in the CBE applet.")
  • CBE django app python API - can convert taxonomies to competencies, provide other competency features ✅ respects layering
  • content_tagging REST API in openedx-platform (openedx/core/djangoapps/content_tagging/rest_api/v1/) - injects and handles the taxonomy_type field (not persisted in the database), which gets routed to the competency python API as needed. ⚠️ breaks layering but this is a fairly minimal "wrapper"/"router" type code anyways so it seems like a logical place to do this
  • Studio MFE frontend: combined handling of taxonomies & competencies as per UX mockups. ⚠️ breaks layering but the product folks think it's better UX than separating them into two separate UIs.

At least in that case there are clean boundaries drawn in several places that completely respect the layering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had Claude create the following comparison table and diagrams to help me think through our options more clearly, with your most recent proposal added as option 4.

I think Option 1 (currently in this ADR) and your option 4 (the router in openedx-platform) are the two strongest contenders at this point. I'm not opposed to going with option 4 if you feel it fits Open edX's existing patterns better and is worth it to avoid an explicit COMPETENCY value in the tagging app's enum.

I think there are two bigger-picture questions underneath this:

  1. If someone runs Open edX without using any CBE features (as many people probably will), is this okay? Jenna and I have already been marketing this CBE initiative as "built into the core, not an add-on," so I think it's completely fair to accept a small trade-off where every Open edX instance carries a tiny bit of CBE awareness even if that instance never uses the feature. On this question, I'd put options 1 and 4 on relatively equal ground, both are small, harmless trade-offs for non-CBE users, though option 1's is arguably a bit lighter since it's inert stored data, while option 4 runs a small check on every request.

  2. If someone wants to add another taxonomy type, is this okay? This situation might actually be pretty unlikely, especially given our recent conversation about getting rid of system-level taxonomies, but given how pluggable Open edX is meant to be, I still think it's a fair question to want a good answer for. Here the two options aren't equal: option 1 is a one-line addition to the model, a new enum value, and the serializer never changes. Option 4 is a new branch added directly into openedx-platform's shared serializer, plus a matching select_related addition to that view's queryset.

Given how close options 1 and 4 are, I think I'd like to bring this to Tuesday's architecture meeting rather than trying to settle it here in the thread. Let me know if that works for you, or if you'd rather wait for Dave, or if one option is starting to stand out to you as more of a clear winner. Also let me know if you have different thinking around the comparison table and what's been proposed than what is outlined below.

Here's the comparison, for reference:

Comparison

# Proposal DB schema Layering Pros Cons
1 ADR: taxonomy_type enum field Taxonomy gets new column No import; enum value name is a soft reference - Filterable/queryable with a plain WHERE
- Serializer just returns stored data, no cross-app knowledge
- Already drafted, one migration
- Extensible: a future third taxonomy type is just a new enum value; the read path never changes again
- Generic app's enum now names a specific downstream concept
- Needs to be set correctly at creation time
2 hasattr check in serializer Unchanged Yes, flagged as a layering violation in review - No migration
- Always accurate, can't drift out of sync
- Generic app's serializer names a sibling app's model relation directly
- Needs a join/exists-subquery to filter by type, no plain indexed WHERE
- N+1 risk: one extra query per row when listing taxonomies unless select_related('competencytaxonomy') is added, and since the check is on the shared base serializer, oel_tagging's own REST API (used by consumers other than openedx-platform) inherits the risk with no equivalent fix
- Not extensible: a future third taxonomy type needs another hardcoded branch added directly to oel_tagging's shared serializer
- Already deprioritized once on #618 (1+3 was favored then); revived without stated reasoning
3a Two-call frontend create flow Unchanged N/A, this is a call-shape question - No new REST surface - Frontend must orchestrate two calls
- Partial-failure handling (taxonomy created, competency conversion fails)
- Already rejected for UX reasons
3b New combined competency REST API Unchanged N/A - Atomic from the frontend's point of view - New endpoint to design, build, maintain
- Frontend still needs both the plain "create taxonomy" call and this combined call, and has to pick between them since nothing tells it upfront whether a Tags or Competency taxonomy is being created
- That branching arguably relocates the type-awareness into the frontend rather than removing it, though it wouldn't formally be called a layering violation there
4 Router: check + create-flow wiring moved into platform's REST view Unchanged Claimed to be none; violation (if any) contained to the integration layer, but it's relocated, not removed - oel_tagging and CBE app both stay fully pure
- One existing endpoint, no new REST surface
- Matches this repo's real rule: openedx-core must never depend on openedx-platform, not the reverse
- Verified against the actual code: TaxonomyOrgView already fully overrides get_queryset() and TaxonomyOrgSerializer already extends the base fields, so both the check and its select_related fix stay entirely in openedx-platform, zero footprint on oel_tagging or its other consumers
- A deployment that never installs the CBE app is completely unaffected, since oel_tagging itself never mentions competencies
- Same query-filtering cost as option 2, no schema change
- Same N+1 risk, but confined to openedx-platform's own endpoint, and fixable there without touching oel_tagging
- Every openedx-platform deployment carries this hardcoded check permanently in its shared content_tagging layer, whether or not that operator ever enables CBE, harmless at runtime, but not absent
- Not extensible: same as option 2, a future third taxonomy type needs another hardcoded branch, just added to openedx-platform's shared serializer instead of oel_tagging's

Legend: 🟩 pure/unchanged · 🟧 competency-specific · 🟦 integration layer · 🟥 violation/hardcoding · 🟨 schema change

Option 1 — persisted enum field

flowchart TD
  FE[Studio MFE]
  subgraph PLAT[openedx-platform]
    P1[REST API]
  end
  subgraph CORE[openedx-core: oel_tagging]
    C1[Serializer]
  end
  subgraph CBEAPP[openedx-core: CBE app]
    B1[CBE API]
  end
  subgraph DB[Database]
    T[("Taxonomy<br/>+ taxonomy_type")]
    CT[(CompetencyTaxonomy)]
  end

  FE --> P1 --> C1
  C1 -->|reads column| T
  B1 -->|writes on create| CT
Loading

Only option where the Taxonomy table's schema changes. Serializer just reads a plain column, no cross-app lookup at read time.

Option 2 — hasattr in oel_tagging's serializer

flowchart TD
  FE[Studio MFE]
  subgraph PLAT[openedx-platform]
    P1[REST API]
  end
  subgraph CORE[openedx-core: oel_tagging]
    C1["Serializer<br/>hasattr check"]
  end
  subgraph CBEAPP[openedx-core: CBE app]
    B1[CBE API]
  end
  subgraph DB[Database]
    T[(Taxonomy<br/>unchanged)]
    CT[(CompetencyTaxonomy)]
  end

  FE --> P1 --> C1
  C1 -->|reads| T
  C1 -.->|"exists? (per request)"| CT
  B1 --> CT
Loading

Taxonomy table is untouched. The generic serializer instead reaches across to check for CompetencyTaxonomy's existence on every read, that cross-app relation name is the violation.

Option 3a — two-call frontend create flow

flowchart TD
  FE[Studio MFE]
  subgraph PLAT[openedx-platform]
    P1[REST API]
  end
  subgraph CORE[openedx-core: oel_tagging]
    C1[create_taxonomy]
  end
  subgraph CBEAPP[openedx-core: CBE app]
    B0[existing conversion call]
  end
  subgraph DB[Database]
    T[(Taxonomy<br/>unchanged)]
    CT[(CompetencyTaxonomy)]
  end

  FE -->|"call 1: create taxonomy"| P1 --> C1 --> T
  FE -->|"call 2: convert to competency"| B0 --> CT
Loading

Schema is unchanged. The frontend makes two sequential calls and has to handle the case where the first succeeds but the second fails.

Option 3b — new combined competency REST API

flowchart TD
  FE["Studio MFE<br/>picks a call based on taxonomy type"]
  subgraph PLAT[openedx-platform]
    P1[REST API: create taxonomy]
  end
  subgraph CORE[openedx-core: oel_tagging]
    C1[create_taxonomy]
  end
  subgraph CBEAPP[openedx-core: CBE app]
    B0N[new combined endpoint]
  end
  subgraph DB[Database]
    T[(Taxonomy<br/>unchanged)]
    CT[(CompetencyTaxonomy)]
  end

  FE -->|"Tags: plain call"| P1 --> C1 --> T
  FE -->|"Competency: combined call"| B0N
  B0N --> T
  B0N --> CT
Loading

Schema is unchanged. One new endpoint handles both tables in one request, but the frontend still needs both this call and the plain create-taxonomy call, and has to choose between them since nothing upstream tells it whether a Tags or Competency taxonomy is being created. That's the type-awareness moving into the frontend rather than disappearing.

Option 4 — router in openedx-platform's REST view

flowchart TD
  FE[Studio MFE]
  subgraph PLAT["openedx-platform (router lives here)"]
    P1["Serializer: adds taxonomy_type<br/>via hasattr, on the same instance"]
  end
  subgraph CORE[openedx-core: oel_tagging]
    C1[Base Taxonomy query/serializer, stays pure]
  end
  subgraph DB[Database]
    T[(Taxonomy<br/>unchanged)]
    CT[(CompetencyTaxonomy)]
  end

  FE --> P1
  P1 -->|delegates for the base object| C1
  C1 --> T
  P1 -.->|"hasattr check on that<br/>same object, no new API call"| CT
Loading

oel_tagging stays untouched and fetches plain Taxonomy rows as it always has, same schema as options 2/3. openedx-platform's serializer runs one extra check on that same already-fetched instance, no second endpoint or separate CBE call needed for GET, since the two tables are already joinable at the database/ORM level. Query-filtering cost is identical to option 2. The already-decided #614 create-flow wiring is left out of this diagram since it applies the same way regardless of which GET option is chosen.

The dotted line from openedx-platform's serializer to CompetencyTaxonomy isn't a network call, a Python import, or a call into any CBE API. It's Django exposing an existing database join: because CompetencyTaxonomy is defined as a subclass of Taxonomy, the two tables are already linked at the schema level, and any code in the same process, including openedx-platform's serializer, can ask "does a matching row exist in the other table?" without ever referencing CBE's code.

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.

Thanks, this is super helpful! Let's discuss on Tuesday.

If someone runs Open edX without using any CBE features (as many people probably will), is this okay?

I think we both agree that any of these approaches are all OK in this case, as the CBE feature has a pretty small footprint on the overall tagging/taxonomy code and UI.

If someone wants to add another taxonomy type, is this okay?

I think this is a really important question and it's what I'm most concerned about. If we set a precedent that taxonomies are a foundational layer and other things can be built on top of them without modifying the taxonomy code or UI, I think there's no problem at all. People can build whatever they imagine on top of tagging and taxonomies. But in the case of something like CBE, I would prefer to see such features built as separate apps, with their own UI, and not "built in" to the tagging/taxonomy code.

We're instead setting the opposite precedent: integrating a new feature with taxonomies involves adding some code directly to the taxonomies backend and integrating custom UI components into the tagging frontend as well.

So we're neither building a generic plugin API nor maintaining a clean abstraction/layering, but instead introducing a bit of coupling of competency + taxonomy app in a few places. (Exactly which places depends on which approach from the options above, but it seems like product folks want the UI integrated in any case.)

So: I think this is reasonable if competencies is the only special "type" of taxonomy that we or anyone else will have in the platform for the foreseeable future. But if we expect other use cases, then we should set a cleaner architectural precedent that builds a proper plugin API for integration (e.g. allows you to inject components into the tagging/taxonomy UI, and register new taxonomy types dynamically) or just builds on top of taxonomies without modifying them at all (e.g. fully separate frontend, and REST APIs, layering respected).


Future considerations
~~~~~~~~~~~~~~~~~~~~~~

If a third taxonomy flavor is introduced later, it extends this same field and enum with a
third value; no schema redesign is implied. This field is not a general-purpose
polymorphism mechanism and does not preclude one being added separately if a future need
requires it.

Rejected Alternatives
----------------------

Check for a related ``CompetencyTaxonomy`` row directly inside ``openedx_tagging``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The original #618 proposal: ``hasattr(instance, "competencytaxonomy")`` inside
``openedx_tagging``'s serializer. Rejected because it hardcodes a specific downstream
applet's multi-table-inheritance relation name into a standalone, generic library, which
is exactly the reverse-dependency problem this decision exists to avoid.

An overridable ``Taxonomy.get_type()`` method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The follow-up #618 proposal: a base ``Taxonomy.get_type()`` method returning ``"tags"``,
overridden by ``CompetencyTaxonomy`` to return ``"competency"``, mirroring the existing
``Taxonomy.system_defined`` / ``SystemDefinedTaxonomy`` base/override shape. Rejected for
two independent reasons:

- That base/override shape is implemented via ``Taxonomy._taxonomy_class`` and
``Taxonomy.cast()``/``Taxonomy.copy()``, which #634 (Remove Taxonomy Subclasses) removes.
- Independent of #634: querying taxonomies the normal way (``Taxonomy.objects.all()``)
returns plain ``Taxonomy`` instances, so a subclass method override is never reached
without an explicit cast step first. The existing ``.cast()``/``.copy()`` implementation
would not correctly perform that cast for a true multi-table-inheritance subclass in any
case: it copies a hardcoded list of base ``Taxonomy`` field values in Python and never
queries the subclass's own table, so it would silently produce a ``CompetencyTaxonomy``
instance with unset or wrong subclass-specific fields.

Frontend-only: no backend field
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Studio determines type by combining ``openedx_tagging``'s generic taxonomy list with a
separate, CBE-owned endpoint client-side. Rejected because it gives no answer to any
in-process backend consumer of this published library (other apps, plugins, management
commands), only to whichever frontend chooses to make two calls and join them, and it
pushes the integration cost of that join onto every future consumer rather than paying it
once.

A settings-configured resolver function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A Django setting pointing at a dotted import path to a resolver function, owned by CBE,
that ``Taxonomy.get_type()`` calls if configured. This would have kept the type-detection
logic entirely out of ``openedx_tagging``'s own source. Rejected because it is a new,
bespoke, ad-hoc hook into ``openedx_tagging``, when this project's established pattern for
a cross-app extension point, if one is genuinely needed, is ``openedx-events``/
``openedx-filters`` rather than a one-off settings hook. Reaching for that established
pattern instead would mean depending on and wiring up a plugin/event framework across
repos for a field with exactly two known values today -- more machinery than either this
alternative or the field approach chosen here, not less.

Changelog
---------

2026-07-16:

* Proposed.