From c416128c474855198204b139c39ec09da96277ba Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Fri, 3 Jul 2026 14:23:29 -0700 Subject: [PATCH 1/5] docs: add ADR for institution-defined tag code field Proposes a generic Tag.code field in openedx_tagging to hold an institution's own competency identifier, plus a completeness rule that requires it for Competency Taxonomy tags without teaching openedx_tagging what a Competency Taxonomy is. Answers issue #625. --- .../decisions/0010-tag-code-field.rst | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/openedx_tagging/decisions/0010-tag-code-field.rst diff --git a/docs/openedx_tagging/decisions/0010-tag-code-field.rst b/docs/openedx_tagging/decisions/0010-tag-code-field.rst new file mode 100644 index 000000000..3a5c8b276 --- /dev/null +++ b/docs/openedx_tagging/decisions/0010-tag-code-field.rst @@ -0,0 +1,182 @@ +.. _openedx-tagging-adr-0010: + +10. Institution-defined tag code field +======================================== + +Status +------ + +Proposed + +Context +------- + +`Issue #625 `_ asks the project to +determine an ID field strategy for Competency-Based Education (CBE) competency data. +Competency Management UX designs show a "Competency ID" on each competency tag: a +short, human-readable identifier an institution can reference in its own systems +(spreadsheets, internal catalogs, etc). Clicking it drives the workflow that +creates a ``CompetencyCriteria`` association between the competency and content +criteria rules. + +This identifier is explicitly **not** a CASE or CTDL identifier. Those are external +standards bodies' IDs that may need their own storage in the future; conflating them +with an institution's internal code would make each harder to reason about +independently. This ADR is scoped to the internal, institution-defined code only. + +``Tag`` already has three identifiers, none of which fit: + +- ``id``: internal primary key, never user-visible. +- ``uuid``: stable external reference, not human-readable or institution-assigned. +- ``external_id``: intended to link a Tag to a record in an externally-defined + taxonomy. It is de facto immutable: no API path mutates it after creation, tag + re-sync code documents it as "must be an immutable ID", and import/export uses it + purely as the lookup key to find and update existing tags on re-import. Repurposing + it as an admin-editable Competency ID would conflict with that existing, load-bearing + contract. + +**Placement constraint.** :ref:`openedx-learning-adr-0001` keeps ``openedx_tagging`` +standalone and rejected putting CBE tables inside it for that reason. A CBE-named +field on ``Tag`` would repeat that mistake, but a single scalar identifier — the same +shape ``external_id`` already fills — does not. Every taxonomy listing, search, and +export already reads ``Tag``; putting a per-tag identifier in a separate app would +force a cross-app join into that common path for what is otherwise a plain string +column. + +Decision +-------- + +Add a new field, ``Tag.code``, to ``openedx_tagging`` as a **generic**, +non-CBE-specific identifier slot, available to any taxonomy: + +- A plain, optional text field, matching ``external_id``'s shape. +- **Uniqueness:** enforced per-taxonomy, not globally unique (different + institutions/taxonomies may reasonably reuse a code) and not advisory-only (the CBE + workflow uses this value as a click target to create a ``CompetencyCriteria`` + association; ambiguity within a taxonomy would be a routing bug, not a + data-quality nit). +- **Assignment:** user-entered when given in initial import; every non-Competency taxonomy leaves + ``code`` fully optional with no fallback. For a Competency Taxonomy, every tag + needs one: creating a new tag without a code — via import, or via the existing + Taxonomy Editing UI, which has no field for it yet — generates a placeholder (e.g. + derived once from the tag's current name and position) instead of blocking the + tag's creation or an otherwise-valid import. If a user forgets to include the code column in their initial import, they can overwrite the placeholder + with their real code through a follow-up re-import, the same way any other field + is corrected. Re-importing an *existing* tag whose code is blank in the file leaves + its current value untouched rather than generating a new one — a blank cell isn't a + request to clear it since that is not allowed for Competency Taxonomies. +- **Completeness for Competency Taxonomies:** every tag in a Competency Taxonomy must + have a ``code``, guaranteed either by an explicit value or the placeholder fallback + above; every other taxonomy leaves it fully optional (see below for how this is + enforced without ``openedx_tagging`` knowing what a Competency Taxonomy is). +- **Read exposure:** ``code`` is exposed read-only outside of import, so it can still + be displayed on the Competency Management page and assignment workflow even though + it isn't editable there yet (planned for a later phase). +- **Migration:** additive only — a new nullable field, no backfill needed. No PII + annotation is required: ``code`` is institution-authored administrative metadata, + not personal data. + +This keeps ``openedx_tagging`` free of CBE-specific concepts: the word "Competency" +never needs to appear in this app. The CBE applet's API and UI are responsible for +labeling ``code`` as "Competency ID" and driving the click-through workflow. + +Completeness rule for Competency Taxonomies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``openedx_tagging`` must not need to know what a "Competency Taxonomy" is to enforce +completeness, per the placement constraint above. The base ``Taxonomy`` class gets a +property that defaults to "not required"; a taxonomy subclass defined outside +``openedx_tagging`` overrides it to require a code. This mirrors the existing +extension pattern already used for system-defined taxonomies, where a subclass living +in another package customizes behavior without ``openedx_tagging`` importing it. The +rule is enforced at the point a tag is saved, so every path that creates one — import, +the existing Taxonomy Editing UI, or the generic API — is covered by one check rather +than one per caller, and none of them can accidentally create a code-less tag. + +Import/export +~~~~~~~~~~~~~~ + +``code`` becomes an optional column in the import file format for every taxonomy. A +new tag with no ``code`` gets the placeholder fallback described above, whether it's +a Competency Taxonomy tag created via import or via the UI. Re-importing an existing +tag never clears or regenerates its ``code`` just because the file's column is blank +for that row; it only changes when the file supplies an explicit new value. + +How a Competency Taxonomy itself gets created is out of this ADR's scope — that is +already covered by separate, in-progress work. This ADR's changes don't depend on +that work landing first, and don't introduce any new API endpoints of their own. + +Rejected Alternatives +---------------------- + +Repurpose ``external_id`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Reuse the existing ``external_id`` field instead of adding a new one. Rejected: +``external_id`` is de facto immutable and is the load-bearing lookup key for +import/export matching and system-defined-taxonomy re-sync. Giving it a second, +editable meaning would break that existing contract. + +CBE-specific field directly on ``Tag`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add a field explicitly named and documented as the "Competency ID" on ``Tag``, as a +deliberate exception to :ref:`openedx-learning-adr-0001`'s boundary. Rejected: this is +the same schema location as the chosen decision, but naming it as a CBE concept bakes +domain semantics into a library meant to stay standalone, for no benefit over the +generic framing. + +Competency-specific field in ``openedx_learning`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add the identifier to a model in the new ``openedx_learning`` app instead, referenced +from ``Tag``. Rejected: this value is display data needed by every competency tag +listing, search, and export. A cross-app join for a plain string column adds cost with +no corresponding benefit, and no table in that app has the right cardinality (one row +per tag, not per taxonomy or per criterion). + +``CompetencyTaxonomy`` as the home +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attach the identifier to the ``CompetencyTaxonomy`` row itself. Rejected on +cardinality: ``CompetencyTaxonomy`` is one row per taxonomy; a Competency ID is one +value per tag. + +Keep every code continuously derived from the tag's name and hierarchy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Treat the code as a live, computed value (e.g. ``PK-1.2``) that stays in sync with +the tag's current name and position, rather than a value the institution owns once +it exists. Rejected: once a tag has any code — real or a placeholder from the +fallback above — only a re-import changes it. Continuously recomputing it on every +rename or move would contradict that and mean the institution never really owns the +value once assigned. + +Global uniqueness +~~~~~~~~~~~~~~~~~~ + +Enforce uniqueness across all taxonomies. Rejected: overreaches into assuming a +single global namespace for institution-chosen codes, which a shared library +shouldn't impose. + +Advisory-only uniqueness +~~~~~~~~~~~~~~~~~~~~~~~~~ + +No database constraint, just a UI warning on collision. Rejected: the field is a +click target for creating a ``CompetencyCriteria`` association; a duplicate within a +taxonomy is a functional ambiguity, not just a data-quality nit. + +Enforce completeness only during import, not at the model level +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Rejected: tags can already be created outside the import path today, through the +existing Taxonomy Editing UI or the generic tag-creation API, neither of which has a +way to supply a code directly. Enforcing the rule only during import would let those +paths silently create a code-less tag instead of falling back to a placeholder. + +Changelog +--------- + +2026-07-03: + +* Proposed. From 97be0aeedf502b1cdbeb68689b31ca47ba906a9e Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Fri, 3 Jul 2026 14:44:30 -0700 Subject: [PATCH 2/5] docs: cbe tag code field --- .../decisions/0010-tag-code-field.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/openedx_tagging/decisions/0010-tag-code-field.rst b/docs/openedx_tagging/decisions/0010-tag-code-field.rst index 3a5c8b276..fbec08819 100644 --- a/docs/openedx_tagging/decisions/0010-tag-code-field.rst +++ b/docs/openedx_tagging/decisions/0010-tag-code-field.rst @@ -35,13 +35,13 @@ independently. This ADR is scoped to the internal, institution-defined code only it as an admin-editable Competency ID would conflict with that existing, load-bearing contract. -**Placement constraint.** :ref:`openedx-learning-adr-0001` keeps ``openedx_tagging`` -standalone and rejected putting CBE tables inside it for that reason. A CBE-named -field on ``Tag`` would repeat that mistake, but a single scalar identifier — the same -shape ``external_id`` already fills — does not. Every taxonomy listing, search, and -export already reads ``Tag``; putting a per-tag identifier in a separate app would -force a cross-app join into that common path for what is otherwise a plain string -column. +**Placement constraint.** :ref:`openedx-tagging-adr-0002` keeps ``openedx_tagging`` +standalone, and :ref:`openedx-learning-adr-0001` already rejected putting CBE tables +inside it for that reason. A CBE-named field on ``Tag`` would repeat that mistake, +but a single scalar identifier — the same shape ``external_id`` already fills — does +not. Every taxonomy listing, search, and export already reads ``Tag``; putting a +per-tag identifier in a separate app would force a cross-app join into that common +path for what is otherwise a plain string column. Decision -------- @@ -121,7 +121,7 @@ CBE-specific field directly on ``Tag`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a field explicitly named and documented as the "Competency ID" on ``Tag``, as a -deliberate exception to :ref:`openedx-learning-adr-0001`'s boundary. Rejected: this is +deliberate exception to :ref:`openedx-tagging-adr-0002`'s boundary. Rejected: this is the same schema location as the chosen decision, but naming it as a CBE concept bakes domain semantics into a library meant to stay standalone, for no benefit over the generic framing. From a9022e1ee14163ceca48bfedf383f9e86037b40d Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Fri, 3 Jul 2026 16:49:46 -0700 Subject: [PATCH 3/5] docs: clarify that CompetencyTaxonomy is a real subclass and does not mirror the system-level taxonomy --- .../decisions/0010-tag-code-field.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/openedx_tagging/decisions/0010-tag-code-field.rst b/docs/openedx_tagging/decisions/0010-tag-code-field.rst index fbec08819..fe6d53535 100644 --- a/docs/openedx_tagging/decisions/0010-tag-code-field.rst +++ b/docs/openedx_tagging/decisions/0010-tag-code-field.rst @@ -85,13 +85,12 @@ Completeness rule for Competency Taxonomies ``openedx_tagging`` must not need to know what a "Competency Taxonomy" is to enforce completeness, per the placement constraint above. The base ``Taxonomy`` class gets a -property that defaults to "not required"; a taxonomy subclass defined outside -``openedx_tagging`` overrides it to require a code. This mirrors the existing -extension pattern already used for system-defined taxonomies, where a subclass living -in another package customizes behavior without ``openedx_tagging`` importing it. The -rule is enforced at the point a tag is saved, so every path that creates one — import, -the existing Taxonomy Editing UI, or the generic API — is covered by one check rather -than one per caller, and none of them can accidentally create a code-less tag. +property that defaults to "not required"; ``CompetencyTaxonomy`` +(:ref:`openedx-learning-adr-0002`), a real subclass of ``Taxonomy`` defined outside +``openedx_tagging``, overrides it to require a code. The rule is enforced at the +point a tag is saved, so every path that creates one — import, the existing Taxonomy +Editing UI, or the generic API — is covered by one check rather than one per caller, +and none of them can accidentally create a code-less tag. Import/export ~~~~~~~~~~~~~~ From d3701bbd808b12f13d1d737e1ce319d44cb63b47 Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Tue, 7 Jul 2026 15:23:54 -0700 Subject: [PATCH 4/5] docs: use mutable external_id instead of a new tag code field PR review and follow-up team discussion converged on making Tag.external_id mutable via a new previous_id import column, rather than adding a separate Tag.code field, to give institutions an editable competency identifier. --- .../decisions/0010-tag-code-field.rst | 230 +++++++----------- 1 file changed, 88 insertions(+), 142 deletions(-) diff --git a/docs/openedx_tagging/decisions/0010-tag-code-field.rst b/docs/openedx_tagging/decisions/0010-tag-code-field.rst index fe6d53535..0ee8acea6 100644 --- a/docs/openedx_tagging/decisions/0010-tag-code-field.rst +++ b/docs/openedx_tagging/decisions/0010-tag-code-field.rst @@ -1,7 +1,7 @@ .. _openedx-tagging-adr-0010: -10. Institution-defined tag code field -======================================== +10. Mutable tag ``external_id`` for institution-defined identifiers +===================================================================== Status ------ @@ -14,168 +14,114 @@ Context `Issue #625 `_ asks the project to determine an ID field strategy for Competency-Based Education (CBE) competency data. Competency Management UX designs show a "Competency ID" on each competency tag: a -short, human-readable identifier an institution can reference in its own systems -(spreadsheets, internal catalogs, etc). Clicking it drives the workflow that +short, human-readable identifier an institution assigns and can reference in its own +systems (spreadsheets, internal catalogs, etc). Clicking it drives the workflow that creates a ``CompetencyCriteria`` association between the competency and content criteria rules. This identifier is explicitly **not** a CASE or CTDL identifier. Those are external standards bodies' IDs that may need their own storage in the future; conflating them -with an institution's internal code would make each harder to reason about -independently. This ADR is scoped to the internal, institution-defined code only. - -``Tag`` already has three identifiers, none of which fit: - -- ``id``: internal primary key, never user-visible. -- ``uuid``: stable external reference, not human-readable or institution-assigned. -- ``external_id``: intended to link a Tag to a record in an externally-defined - taxonomy. It is de facto immutable: no API path mutates it after creation, tag - re-sync code documents it as "must be an immutable ID", and import/export uses it - purely as the lookup key to find and update existing tags on re-import. Repurposing - it as an admin-editable Competency ID would conflict with that existing, load-bearing - contract. - -**Placement constraint.** :ref:`openedx-tagging-adr-0002` keeps ``openedx_tagging`` -standalone, and :ref:`openedx-learning-adr-0001` already rejected putting CBE tables -inside it for that reason. A CBE-named field on ``Tag`` would repeat that mistake, -but a single scalar identifier — the same shape ``external_id`` already fills — does -not. Every taxonomy listing, search, and export already reads ``Tag``; putting a -per-tag identifier in a separate app would force a cross-app join into that common -path for what is otherwise a plain string column. +with an institution's internal identifier would make each harder to reason about +independently. This ADR is scoped to the internal, institution-defined identifier +only, and assumes one such identifier per tag: no LMS surveyed for this ADR supports +multiple institution identifiers for the same competency. + +Institutions do rename these identifiers: when a competency taxonomy is revised (for +example, after a curriculum or accreditation-standard update), the existing tags get +renumbered in place rather than the institution creating a brand new taxonomy for the +same competencies. Any solution needs to survive that rename without losing the tag's +existing associations (e.g. its ``CompetencyCriteria`` links) or its import/export +identity. + +``Tag`` already has an ``external_id`` field, used to link a Tag to a record in an +externally-defined taxonomy. The name reflects that the identifier is external to +Open edX, not external to the institution: the institution that assigns it is free to +use it for exactly this kind of internal, institution-defined identifier. Today, +though, ``external_id`` is de facto immutable: no API path mutates it after creation, +tag re-sync code documents it as "must be an immutable ID", and import/export uses it +purely as the lookup key to find and update existing tags on re-import. Decision -------- -Add a new field, ``Tag.code``, to ``openedx_tagging`` as a **generic**, -non-CBE-specific identifier slot, available to any taxonomy: - -- A plain, optional text field, matching ``external_id``'s shape. -- **Uniqueness:** enforced per-taxonomy, not globally unique (different - institutions/taxonomies may reasonably reuse a code) and not advisory-only (the CBE - workflow uses this value as a click target to create a ``CompetencyCriteria`` - association; ambiguity within a taxonomy would be a routing bug, not a - data-quality nit). -- **Assignment:** user-entered when given in initial import; every non-Competency taxonomy leaves - ``code`` fully optional with no fallback. For a Competency Taxonomy, every tag - needs one: creating a new tag without a code — via import, or via the existing - Taxonomy Editing UI, which has no field for it yet — generates a placeholder (e.g. - derived once from the tag's current name and position) instead of blocking the - tag's creation or an otherwise-valid import. If a user forgets to include the code column in their initial import, they can overwrite the placeholder - with their real code through a follow-up re-import, the same way any other field - is corrected. Re-importing an *existing* tag whose code is blank in the file leaves - its current value untouched rather than generating a new one — a blank cell isn't a - request to clear it since that is not allowed for Competency Taxonomies. -- **Completeness for Competency Taxonomies:** every tag in a Competency Taxonomy must - have a ``code``, guaranteed either by an explicit value or the placeholder fallback - above; every other taxonomy leaves it fully optional (see below for how this is - enforced without ``openedx_tagging`` knowing what a Competency Taxonomy is). -- **Read exposure:** ``code`` is exposed read-only outside of import, so it can still - be displayed on the Competency Management page and assignment workflow even though - it isn't editable there yet (planned for a later phase). -- **Migration:** additive only — a new nullable field, no backfill needed. No PII - annotation is required: ``code`` is institution-authored administrative metadata, - not personal data. - -This keeps ``openedx_tagging`` free of CBE-specific concepts: the word "Competency" -never needs to appear in this app. The CBE applet's API and UI are responsible for -labeling ``code`` as "Competency ID" and driving the click-through workflow. - -Completeness rule for Competency Taxonomies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``openedx_tagging`` must not need to know what a "Competency Taxonomy" is to enforce -completeness, per the placement constraint above. The base ``Taxonomy`` class gets a -property that defaults to "not required"; ``CompetencyTaxonomy`` -(:ref:`openedx-learning-adr-0002`), a real subclass of ``Taxonomy`` defined outside -``openedx_tagging``, overrides it to require a code. The rule is enforced at the -point a tag is saved, so every path that creates one — import, the existing Taxonomy -Editing UI, or the generic API — is covered by one check rather than one per caller, -and none of them can accidentally create a code-less tag. - -Import/export -~~~~~~~~~~~~~~ - -``code`` becomes an optional column in the import file format for every taxonomy. A -new tag with no ``code`` gets the placeholder fallback described above, whether it's -a Competency Taxonomy tag created via import or via the UI. Re-importing an existing -tag never clears or regenerates its ``code`` just because the file's column is blank -for that row; it only changes when the file supplies an explicit new value. - -How a Competency Taxonomy itself gets created is out of this ADR's scope — that is -already covered by separate, in-progress work. This ADR's changes don't depend on -that work landing first, and don't introduce any new API endpoints of their own. +Use ``Tag.external_id`` as the institution's editable competency identifier, and +enable a pathway for its value to change, rather than adding a new field. + +- The mutability pathway is import-only for now: the tag import file format gains a + new optional column, ``previous_id``. When a row's ``previous_id`` matches an + existing tag's ``external_id`` within the taxonomy, and the row's ``id`` differs + from it, the import treats the row as a rename: it updates that tag's + ``external_id`` to the new ``id`` (and any other changed fields) in place, instead + of deleting the old tag and creating a new one. This preserves the tag's existing + associations and its import/export identity across the rename. +- Editing ``external_id`` through the Taxonomy Editing UI or the generic + tag-update REST API is anticipated as future-phase work. Both are unchanged by + this decision for now. +- ``previous_id`` is import-only: it is not a new persisted field and does not + appear in exports. Institutions should maintain their own record of prior + identifiers for now; keeping that history within Open edX itself could be a future + phase of work. +- The existing per-taxonomy uniqueness constraint on ``external_id`` + (``unique_together`` on ``(taxonomy, external_id)``) is unchanged and still applies + to a rename: if the new ``id`` collides with a different existing tag in the same + taxonomy, the import rejects the row, the same way a duplicate ``external_id`` on + tag creation already does today. +- No schema change and no migration: ``external_id`` already permits writes at the + model layer, and ``previous_id`` is read per-row from the import file and consumed + only while generating the import plan. + +Future considerations +~~~~~~~~~~~~~~~~~~~~~~ + +Merging multiple existing tags into one is a future phase of work that is not precluded by this +decision. Its semantics (what happens to each old tag's associations, which tag's +other fields win) would be defined in a future document. ``previous_id`` is a transient import-file +column, not a database field name, so supporting multiple prior identifiers later +likely wouldn't require renaming it or introducing a new column: it could just accept +multiple values under the same ``previous_id`` key when that need arises. +Alternatively, renaming it to ``previous_ids`` at that point wouldn't be too costly +either, since it's an import-format detail rather than a stored field. Rejected Alternatives ---------------------- -Repurpose ``external_id`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add a new ``Tag.code`` field +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Reuse the existing ``external_id`` field instead of adding a new one. Rejected: -``external_id`` is de facto immutable and is the load-bearing lookup key for -import/export matching and system-defined-taxonomy re-sync. Giving it a second, -editable meaning would break that existing contract. +An earlier draft of this ADR proposed a generic, per-taxonomy-unique identifier +field, separate from ``external_id``, to get a mutable identifier without disturbing +``external_id``'s immutability contract. Community review converged on relaxing +``external_id`` itself instead: a second field would equal ``external_id`` in value +the vast majority of the time, reading as duplication to end users. -CBE-specific field directly on ``Tag`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Separate ``CompetencyMetadata`` table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a field explicitly named and documented as the "Competency ID" on ``Tag``, as a -deliberate exception to :ref:`openedx-tagging-adr-0002`'s boundary. Rejected: this is -the same schema location as the chosen decision, but naming it as a CBE concept bakes -domain semantics into a library meant to stay standalone, for no benefit over the -generic framing. +Proposed during review as a home for competency-specific tag fields, analogous to +``CompetencyTaxonomy``. Rejected: no competency-specific tag metadata exists today +beyond the identifier this ADR handles via ``external_id``. If a descriptive field +(e.g. a tag description or competency statement) is needed later, it belongs directly +on ``Tag`` as a generic field, since other taxonomies could reasonably want tag +descriptions too; a competency-specific table would need its own justification if +and when competency-specific metadata actually arises. -Competency-specific field in ``openedx_learning`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Keep ``external_id`` strictly immutable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the identifier to a model in the new ``openedx_learning`` app instead, referenced -from ``Tag``. Rejected: this value is display data needed by every competency tag -listing, search, and export. A cross-app join for a plain string column adds cost with -no corresponding benefit, and no table in that app has the right cardinality (one row -per tag, not per taxonomy or per criterion). - -``CompetencyTaxonomy`` as the home -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Attach the identifier to the ``CompetencyTaxonomy`` row itself. Rejected on -cardinality: ``CompetencyTaxonomy`` is one row per taxonomy; a Competency ID is one -value per tag. - -Keep every code continuously derived from the tag's name and hierarchy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Treat the code as a live, computed value (e.g. ``PK-1.2``) that stays in sync with -the tag's current name and position, rather than a value the institution owns once -it exists. Rejected: once a tag has any code — real or a placeholder from the -fallback above — only a re-import changes it. Continuously recomputing it on every -rename or move would contradict that and mean the institution never really owns the -value once assigned. - -Global uniqueness -~~~~~~~~~~~~~~~~~~ - -Enforce uniqueness across all taxonomies. Rejected: overreaches into assuming a -single global namespace for institution-chosen codes, which a shared library -shouldn't impose. - -Advisory-only uniqueness -~~~~~~~~~~~~~~~~~~~~~~~~~ - -No database constraint, just a UI warning on collision. Rejected: the field is a -click target for creating a ``CompetencyCriteria`` association; a duplicate within a -taxonomy is a functional ambiguity, not just a data-quality nit. - -Enforce completeness only during import, not at the model level -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Rejected: tags can already be created outside the import path today, through the -existing Taxonomy Editing UI or the generic tag-creation API, neither of which has a -way to supply a code directly. Enforcing the rule only during import would let those -paths silently create a code-less tag instead of falling back to a placeholder. +Treat every ``external_id`` change as a delete-and-recreate instead of relaxing the +immutability contract. Rejected: this loses the tag's existing associations (e.g. its +``CompetencyCriteria`` links) and its import/export identity, exactly the problem +institutions hit when they rename an identifier. Changelog --------- +2026-07-07: + +* Revised: dropped the new ``Tag.code`` field. ``Tag.external_id`` becomes mutable + instead, through a new ``previous_id`` import column, per PR review and follow-up + team discussion. + 2026-07-03: * Proposed. From c343c7edb33ade6f2003bd79e0b46347fed21971 Mon Sep 17 00:00:00 2001 From: Mary Gwozdz Date: Tue, 7 Jul 2026 15:25:18 -0700 Subject: [PATCH 5/5] docs: rename ADR 0010 file to drop code-field reference The decision no longer adds a Tag.code field, so the filename should reflect the mutable external_id approach instead. --- .../{0010-tag-code-field.rst => 0010-mutable-tag-external-id.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/openedx_tagging/decisions/{0010-tag-code-field.rst => 0010-mutable-tag-external-id.rst} (100%) diff --git a/docs/openedx_tagging/decisions/0010-tag-code-field.rst b/docs/openedx_tagging/decisions/0010-mutable-tag-external-id.rst similarity index 100% rename from docs/openedx_tagging/decisions/0010-tag-code-field.rst rename to docs/openedx_tagging/decisions/0010-mutable-tag-external-id.rst