Skip to content

feat(resolvers): implement post-coordinated name resolution - #955

Open
filiperochalopes wants to merge 1 commit into
OpenConceptLab:masterfrom
filiperochalopes:feature/create-postcoordination-name-resolver
Open

filiperochalopes wants to merge 1 commit into
OpenConceptLab:masterfrom
filiperochalopes:feature/create-postcoordination-name-resolver

Conversation

@filiperochalopes

Copy link
Copy Markdown
Contributor

Closes OpenConceptLab/ocl_issues#2951

image

TL;DR: Adds a configurable, read-time names resolver (Source.resolvers) that shows a display name for
post-coordinated codes (ICD-11 A&B/C, SNOMED CT, LOINC) in mappings, with no stored data, no new endpoint and no
frontend change.


Main Changes

Area Files Description
Resolvers package core/resolvers/core.py (+153), core/resolvers/names.py (+194) Base Resolver (config validation, access check, batched concept lookup, cache) and NamesResolver (replace rules, literals, {{name}}). One module per family; new family = subclass + registry entry.
Source config core/sources/models.py, core/sources/serializers.py, core/sources/views.py, core/sources/migrations/0047_source_resolvers.py Nullable JSON column Source.resolvers, validated in clean() and in the version serializer (POST of a version skips full_clean). New versions inherit it from HEAD.
Mapping adapter core/mappings/name_resolution.py (+110), core/mappings/models.py, core/mappings/serializers.py, core/concepts/serializers.py Picks the Source/version per side, decodes the stored code once, resolves a whole page in one batch, caches per user. Fills from/to_concept_name_resolved and cascade_target_concept_name.
Tests core/resolvers/tests.py, core/integration_tests/tests_sources.py, tests_mappings.py, test_utils.py 4 resolver tests, 3 Source API tests, 3 Mapping API tests; query-count guard keeps the same row types in both pages.
Docs DOCS_RESOLVERS.md Design, format, algorithm, limits, future concepts/mappings families.

Configuration format

{"names": {
  "replace":  [{"pattern": "&", "substitution": " [{{name}}]"}],
  "literals": [{"start": "{", "end": "}"}]
}}
  • replace (required, 1–16 rules): pattern (1–8 chars, no whitespace) → substitution (0–32 chars). {{name}}
    (at most once) receives the name of the code or literal right after the pattern.
  • literals (optional, 0–4 pairs): text between start and end is copied without the delimiters, never looked up.
    A delimiter cannot also be a pattern.

Example — ICD-11

{"names": {"replace": [
  {"pattern": "&", "substitution": " [{{name}}]"},
  {"pattern": "/", "substitution": " / "}]}}

ND13.2&XJ44E/ND13.3&XJ44E → Fracture of unspecified tarsal bone [Closed fracture] / Fracture of metatarsal bone [Closed fracture]

Example — SNOMED CT

{"names": {"replace": [
  {"pattern": ":", "substitution": " with "},
  {"pattern": "{", "substitution": ""},
  {"pattern": "}", "substitution": ""},
  {"pattern": ",", "substitution": " and "},
  {"pattern": "=", "substitution": ": "}]}}

71388002:{260686004=129304002,405813007=15497006} → Procedure with Method: Excision - action and Procedure site - Direct: Ovarian structure

Example — LOINC

{"names": {
  "literals": [{"start": "{", "end": "}"}],
  "replace": [
    {"pattern": ":", "substitution": ", "},
    {"pattern": "=", "substitution": ": "},
    {"pattern": ",", "substitution": ", "}]}}

104765-3:{Specimen}=LP7681-2,{Molgen}=LP6464-4 → <name of 104765-3>, Specimen: <name of LP7681-2>, Molgen: <name of LP6464-4>
(the axis in braces is a label, copied without braces; the LP… parts must exist in the destination Source).


Name fields on a mapping

Field Meaning
to_concept_name / from_concept_name Name stored with the mapping (user input or import). Untouched.
to_concept_name_resolved / from_concept_name_resolved Computed on read: the resolver's name, else the linked concept's display_name. Omitted when neither exists, as before.
cascade_target_concept_name Target name in brief/cascade: stored name first, else the resolver's.

Key Decisions

Decision Choice Rationale
Where the config lives Own column Source.resolvers, one key per family Behavior needs a validated schema; meta is display metadata.
Wildcard {{name}} Same interpolation convention as i18next in oclweb3.
Literal output Without delimiters ({Specimen} → Specimen) Reads as a label; enables the LOINC case.
Endpoint None (no $resolve-names) Not FHIR; the name surfaces in existing fields.
Matching Exact concept first, then composition, all-or-nothing Never a partial title; existing behavior wins.
Persistence / access Nothing stored; runs with the requesting user's access Private Sources resolve only for users who can view them.
Invalid stored config Ignored on read, rejected with 400 on write A bad config never causes a 500.
Field names Not renamed Public contract used by oclweb3, ocl-mapper, exports.
Naming in code No terminology names in identifiers Use cases appear only in comments, docs and fixtures.

Verification Checklist

docker exec oclapi2-api-1 python manage.py test core.resolvers --keepdb -v2
docker exec oclapi2-api-1 python manage.py test core.sources core.mappings core.concepts \
  core.integration_tests.tests_mappings core.integration_tests.tests_concepts --keepdb
docker exec -e EXPORT_SERVICE=core.services.storages.cloud.aws.S3 oclapi2-api-1 \
  python manage.py test core.integration_tests.tests_sources --keepdb
docker exec oclapi2-api-1 pylint -j2 core/resolvers core/mappings core/sources core/concepts/serializers.py \
  core/integration_tests/tests_sources.py core/integration_tests/tests_mappings.py core/integration_tests/test_utils.py
docker exec oclapi2-api-1 python manage.py makemigrations sources --check --dry-run

Results on this branch: resolvers 4 OK · regression 714 OK · tests_sources 120 OK · pylint 10.00/10 · no pending
sources migration (the map_projects pending migration is pre-existing).

  • The three use cases resolve from the Source config (ICD-11, SNOMED CT, LOINC)
  • Missing part → no name; an existing concept wins; without resolvers nothing is split
  • Source API: write, reject invalid (400, nothing saved), clear with null, inherit in a new version
  • Mapping API: list, detail, versions, brief and $cascade show the composed name; stored name/URL intact
  • Private destination resolves only for users who can view it
  • Local smoke on WHO/ICD-11-WHO: NC52.10&XA5R12 → Laceration without foreign body of other parts of wrist or hand [Hand]
  • Regression: query-count guards on a real staging dataset
  • Deployment: migration 0047 is a nullable AddField; no index, no reindex

Notes for the reviewer

  • Behavior differences vs. the first prototype: literals are new; a pattern with an empty substitution may sit at an
    end of the expression (SNOMED ends in }); {{name}} with nothing after it gives no name.
  • Mappings pinned to a Source version read that version's resolvers, not HEAD's.
  • Out of scope: resolvers.concepts/mappings, FHIR $lookup, frontend changes, any production configuration.

* Introduce a dedicated mechanism to resolve display names for concepts and mappings, including complex compositions

* This allows the system to display human-readable names derived from multiple components (e.g., a pattern and a placeholder) in a single code

* The resolution process is cacheable and is designed to fall back to the stored/original code name if a part of the composition is missing
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T20:40:56.331214Z
We couldn’t complete this review. Comment @codex review to try again.
11e9c12 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the names resolver for post-coordinated codes (Source.resolvers)

1 participant