From f03f474705535f5605bac467acb6abf470fb6a6c Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Tue, 6 Oct 2026 09:44:05 -0300 Subject: [PATCH 1/2] fix(ocl): translate OCL map types to FHIR R5 relationship codes OCL NARROWER-THAN / BROADER-THAN were emitted as the non-FHIR codes 'narrower-than' / 'broader-than', so $translate discarded them. Map them to the R5 ConceptMapRelationship codes (source-is-narrower-than-target, source-is-broader-than-target), accept NOT-SAME-AS as not-related-to, and normalise spacing/underscore variants of the map type. Co-Authored-By: Claude Opus 5.5 --- tests/ocl/ocl-cm-provider.test.js | 17 +++++++++++++++++ tx/ocl/cm-ocl.cjs | 9 ++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/ocl/ocl-cm-provider.test.js b/tests/ocl/ocl-cm-provider.test.js index a1e2fc4a..b47202ea 100644 --- a/tests/ocl/ocl-cm-provider.test.js +++ b/tests/ocl/ocl-cm-provider.test.js @@ -133,6 +133,23 @@ describe('OCLConceptMapProvider', () => { expect(getMock).toHaveBeenCalledWith('/mappings/map-1/'); }); + it.each([ + ['SAME-AS', 'equivalent'], + ['NARROWER-THAN', 'source-is-narrower-than-target'], + ['BROADER-THAN', 'source-is-broader-than-target'], + ['NOT-SAME-AS', 'not-related-to'], + ['NOT-EQUIVALENT', 'not-related-to'], + ['narrower than', 'source-is-narrower-than-target'], + ['Q-AND-A', 'related-to'], + [undefined, 'related-to'] + ])('should translate OCL map_type %s to FHIR relationship %s', async (mapType, expected) => { + const getMock = jest.fn().mockResolvedValue({ data: makeMapping({ map_type: mapType }) }); + const provider = createProvider({ get: getMock }); + + const result = await provider.fetchConceptMapById('map-1'); + expect(result.jsonObj.group[0].element[0].target[0].relationship).toBe(expected); + }); + it('should return null on HTTP error', async () => { const getMock = jest.fn().mockRejectedValue(new Error('404')); const provider = createProvider({ get: getMock }); diff --git a/tx/ocl/cm-ocl.cjs b/tx/ocl/cm-ocl.cjs index d726ef0a..9903606b 100644 --- a/tx/ocl/cm-ocl.cjs +++ b/tx/ocl/cm-ocl.cjs @@ -453,15 +453,18 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider { return new ConceptMap(json, 'R5'); } + // OCL map_type expresses "from concept to concept", which lines up + // with the R5 ConceptMapRelationship codes (source target). #toRelationship(mapType) { - switch ((mapType || '').toUpperCase()) { + switch (String(mapType || '').trim().toUpperCase().replace(/[\s_]+/g, '-')) { case 'SAME-AS': return 'equivalent'; case 'NARROWER-THAN': - return 'narrower-than'; + return 'source-is-narrower-than-target'; case 'BROADER-THAN': - return 'broader-than'; + return 'source-is-broader-than-target'; case 'NOT-EQUIVALENT': + case 'NOT-SAME-AS': return 'not-related-to'; default: return 'related-to'; From 396e2dc9a0666d1e07b6ef029faa5891e243ea3f Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Tue, 6 Oct 2026 13:04:51 -0300 Subject: [PATCH 2/2] fix(ocl): stop declaring OCL repo paths as ConceptMap scopes $translate resolves a ConceptMap's sourceScope as a ValueSet and checks the code against it, failing the whole operation with not-found when it cannot. The OCL provider set sourceScopeUri/targetScopeUri to OCL repo paths (from_source_url, from_collection_url) or, for aggregated maps, to CodeSystem canonicals, none of which is a ValueSet, so every OCL translation failed: The value set '/orgs/MS/sources/BRCIAP2/', which is the source scope of the ConceptMap '/orgs/MS/sources/BRCIAP2/mappings/6888/', could not be found Leave the scopes unset; the group's source/target carry the systems. Verified against the live OCL instance: BRCIAP2 K86 -> BRCID10 I10, A99 -> 40 matches. Co-Authored-By: Claude Opus 5.5 --- tests/ocl/ocl-cm-provider.test.js | 18 ++++++++++++++++++ tx/ocl/cm-ocl.cjs | 10 ++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/ocl/ocl-cm-provider.test.js b/tests/ocl/ocl-cm-provider.test.js index b47202ea..796898c3 100644 --- a/tests/ocl/ocl-cm-provider.test.js +++ b/tests/ocl/ocl-cm-provider.test.js @@ -150,6 +150,21 @@ describe('OCLConceptMapProvider', () => { expect(result.jsonObj.group[0].element[0].target[0].relationship).toBe(expected); }); + it('should not declare a source/target scope from OCL repo paths', async () => { + const mapping = makeMapping({ + from_collection_url: '/orgs/TestOrg/collections/ColA/', + to_collection_url: '/orgs/TestOrg/collections/ColB/' + }); + const getMock = jest.fn().mockResolvedValue({ data: mapping }); + const provider = createProvider({ get: getMock }); + + const result = await provider.fetchConceptMapById('map-1'); + // $translate resolves a scope as a ValueSet; an OCL path would fail with not-found + expect(result.sourceScope).toBeUndefined(); + expect(result.targetScope).toBeUndefined(); + expect(result.jsonObj.group[0].source).toBe('/orgs/TestOrg/sources/SourceA/'); + }); + it('should return null on HTTP error', async () => { const getMock = jest.fn().mockRejectedValue(new Error('404')); const provider = createProvider({ get: getMock }); @@ -272,6 +287,9 @@ describe('OCLConceptMapProvider', () => { expect(cm.jsonObj.group[0].element).toHaveLength(2); expect(cm.jsonObj.group[0].source).toBe('http://example.org/SourceA'); expect(cm.jsonObj.group[0].target).toBe('http://example.org/SourceB'); + // a CodeSystem canonical is not a ValueSet, so it must not be declared as the scope + expect(cm.sourceScope).toBeUndefined(); + expect(cm.targetScope).toBeUndefined(); }); it('should handle target-system parameter', async () => { diff --git a/tx/ocl/cm-ocl.cjs b/tx/ocl/cm-ocl.cjs index 9903606b..db528186 100644 --- a/tx/ocl/cm-ocl.cjs +++ b/tx/ocl/cm-ocl.cjs @@ -245,8 +245,8 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider { name: id, title: `${sourceId} to ${targetId}`, status: 'active', - sourceScopeUri: group.sourceCanonical, - targetScopeUri: group.targetCanonical, + // No sourceScope/targetScope: a scope is a ValueSet, and $translate resolves it and + // checks the code against it. The systems are carried by the group instead. group: [{ source: group.sourceCanonical, target: group.targetCanonical, @@ -422,8 +422,10 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider { name: `mapping-${id}`, title: mapping.name || `Mapping ${id}`, status: 'active', - sourceScopeUri: mapping.from_collection_url || mapping.fromCollectionUrl || source, - targetScopeUri: mapping.to_collection_url || mapping.toCollectionUrl || target, + // No sourceScope/targetScope: a scope is a ValueSet canonical, which $translate resolves + // and checks the code against. OCL only offers repo paths here (from_source_url or + // from_collection_url), which resolve to no ValueSet and made every translation fail + // with not-found. The systems are carried by the group instead. group: [ { source: sourceCanonical,