diff --git a/docs/openedx_tagging/decisions/0010-mutable-tag-external-id.rst b/docs/openedx_tagging/decisions/0010-mutable-tag-external-id.rst new file mode 100644 index 000000000..0ee8acea6 --- /dev/null +++ b/docs/openedx_tagging/decisions/0010-mutable-tag-external-id.rst @@ -0,0 +1,127 @@ +.. _openedx-tagging-adr-0010: + +10. Mutable tag ``external_id`` for institution-defined identifiers +===================================================================== + +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 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 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 +-------- + +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 +---------------------- + +Add a new ``Tag.code`` field +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +Separate ``CompetencyMetadata`` table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +Keep ``external_id`` strictly immutable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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.