fhirsmith 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +105 -30
  3. package/config-template.json +130 -4
  4. package/extension-tracker/extension-tracker.js +111 -3
  5. package/extension-tracker/readme.md +3 -0
  6. package/ips/ai-client.js +263 -0
  7. package/ips/composer.js +805 -0
  8. package/ips/editor-schema.js +289 -0
  9. package/ips/editor.js +290 -0
  10. package/ips/fhirpath-ips.js +397 -0
  11. package/ips/ips-codeable-concepts.md +1742 -0
  12. package/ips/ips-template.html +125 -0
  13. package/ips/ips-valuesets.js +159 -0
  14. package/ips/ips.js +1736 -0
  15. package/ips/membership.js +179 -0
  16. package/ips/pipeline.js +1012 -0
  17. package/ips/presenter.js +974 -0
  18. package/ips/profile-editor.js +578 -0
  19. package/ips/profiles.js +506 -0
  20. package/ips/readme.md +460 -0
  21. package/ips/render.js +1482 -0
  22. package/ips/search-params.js +83 -0
  23. package/ips/search.js +667 -0
  24. package/ips/share.js +178 -0
  25. package/ips/store.js +650 -0
  26. package/ips/terminology.js +395 -0
  27. package/ips/tx-client.js +202 -0
  28. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  29. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  31. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  32. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  33. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  35. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  36. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  37. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  38. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  39. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  40. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  42. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  43. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  44. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  45. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  46. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  47. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  49. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  50. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  51. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  52. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  53. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  54. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  55. package/ips/valuesets/elements.json +44 -0
  56. package/library/daily-report.js +422 -0
  57. package/library/fhir-search-values.js +138 -0
  58. package/library/html-server.js +32 -23
  59. package/library/html.js +9 -0
  60. package/library/mailer.js +77 -0
  61. package/library/narrative-check.js +222 -0
  62. package/library/npm-audit.js +25 -0
  63. package/library/package-manager.js +4 -2
  64. package/library/sqlite-session-store.js +29 -1
  65. package/package.json +8 -2
  66. package/packages/package-crawler.js +189 -82
  67. package/packages/packages.js +36 -63
  68. package/packages/utilities/dedupe-versions.js +22 -8
  69. package/publisher/publisher.js +29 -0
  70. package/registry/auth-store.js +366 -0
  71. package/registry/auth.js +872 -0
  72. package/registry/crawler.js +48 -1
  73. package/registry/documentation/api.md +162 -0
  74. package/registry/documentation/configuration.md +93 -0
  75. package/registry/documentation/documents.md +74 -0
  76. package/registry/documentation/internals.md +151 -0
  77. package/registry/documentation/readme.md +100 -0
  78. package/registry/documentation/security.md +199 -0
  79. package/registry/documents.js +309 -0
  80. package/registry/openapi.yaml +30 -0
  81. package/registry/registry-template.html +1 -1
  82. package/registry/registry.js +247 -54
  83. package/registry/uptime.js +154 -0
  84. package/release.sh +80 -0
  85. package/server.js +105 -44
  86. package/shl/shl.js +64 -9
  87. package/stats-db.js +93 -0
  88. package/stats.js +158 -1
  89. package/testing/readme.md +1 -1
  90. package/testing/search.js +2 -108
  91. package/testing/store.js +5 -0
  92. package/testing/testing.js +20 -0
  93. package/token/token-template.html +5 -4
  94. package/token/token.js +17 -10
  95. package/translations/Messages.properties +9 -0
  96. package/tx/README.md +52 -0
  97. package/tx/cm/cm-database.js +62 -0
  98. package/tx/cm/cm-package.js +20 -3
  99. package/tx/cs/cs-api.js +90 -14
  100. package/tx/cs/cs-cs.js +142 -19
  101. package/tx/cs/cs-icd11.js +366 -112
  102. package/tx/cs/cs-icf.js +418 -0
  103. package/tx/cs/cs-loinc.js +79 -39
  104. package/tx/cs/cs-omop.js +10 -3
  105. package/tx/cs/cs-snomed.js +918 -55
  106. package/tx/cs/cs-ucum.js +6 -0
  107. package/tx/cs/icd11-expressions.js +191 -78
  108. package/tx/importers/export-icd11-codesystem.js +58 -12
  109. package/tx/importers/icd11-schema.md +9 -5
  110. package/tx/library.js +107 -6
  111. package/tx/ocl/README.md +62 -6
  112. package/tx/ocl/cm-ocl.cjs +166 -44
  113. package/tx/ocl/cs-ocl.cjs +163 -13
  114. package/tx/ocl/resolve/public-access.js +143 -0
  115. package/tx/ocl/resolve/reference-resolver.js +458 -0
  116. package/tx/ocl/vs-ocl.cjs +167 -4
  117. package/tx/openapi-operations.js +4 -0
  118. package/tx/params.js +8 -1
  119. package/tx/problems.js +7 -5
  120. package/tx/provider.js +21 -5
  121. package/tx/sct/documentation.md +251 -0
  122. package/tx/sct/ecl.js +758 -46
  123. package/tx/sct/expressions.js +113 -52
  124. package/tx/syndication/syndication-feed.js +218 -0
  125. package/tx/syndication/syndication-store.js +415 -0
  126. package/tx/tx-html.js +37 -8
  127. package/tx/tx.fhir.org.yml +2 -3
  128. package/tx/tx.js +148 -5
  129. package/tx/vs/vs-database.js +26 -0
  130. package/tx/vs/vs-package.js +42 -10
  131. package/tx/vs/vs-vsac.js +25 -6
  132. package/tx/workers/expand.js +34 -7
  133. package/tx/workers/lookup.js +61 -6
  134. package/tx/workers/metadata.js +6 -1
  135. package/tx/workers/subsumes.js +1 -1
  136. package/tx/workers/translate.js +166 -15
  137. package/tx/workers/validate.js +128 -6
  138. package/utilities/make-sct-test-extension.js +264 -0
  139. package/utilities/submit-test-report.js +1 -1
  140. package/xig/xig.js +120 -12
  141. package/gr.sh +0 -3
  142. package/registry/readme.md +0 -124
@@ -26,6 +26,10 @@ function elementComment(em) {
26
26
  return em.comment || Extensions.readString(em, EXT_ELEMENT_COMMENT);
27
27
  }
28
28
 
29
+ // marks a match whose target came from the code system itself (translateUsingCodeSystem), so
30
+ // checkTargetConcepts leaves it alone. A symbol key, so it never reaches the JSON output
31
+ const TRUSTED_TARGET = Symbol('trustedTarget');
32
+
29
33
  class TranslateWorker extends TerminologyWorker {
30
34
  /**
31
35
  * @param {OperationContext} opContext - Operation context
@@ -281,10 +285,10 @@ class TranslateWorker extends TerminologyWorker {
281
285
  if (conceptMaps.length == 0) {
282
286
  if (reverse) {
283
287
  await this.findConceptMapsInAdditionalResources(conceptMaps,targetSystem, targetScope, sourceScope, coding.system);
284
- await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, targetSystem, targetScope, sourceScope, coding.system, coding.code);
288
+ await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, targetSystem, null, targetScope, sourceScope, coding.system, coding.code);
285
289
  } else {
286
290
  await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
287
- await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem, coding.code);
291
+ await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, coding.version, sourceScope, targetScope, targetSystem, coding.code);
288
292
  }
289
293
  }
290
294
 
@@ -390,7 +394,7 @@ class TranslateWorker extends TerminologyWorker {
390
394
  }
391
395
 
392
396
  // Perform the translation
393
- const result = await this.doTranslate(scoped.maps, coding, targetScope, targetSystem, params);
397
+ const result = await this.doTranslate(scoped.maps, coding, targetScope, targetSystem, txp);
394
398
  return res.status(200).json(result);
395
399
  }
396
400
 
@@ -419,7 +423,11 @@ class TranslateWorker extends TerminologyWorker {
419
423
  // speak for another group, and does not suppress another group's fallback.
420
424
  for (const g of cm.listGroupsInScope(coding, targetScope, targetSystem)) {
421
425
  let groupResult = false;
422
- for (const em of (g.element || []).filter(e => e.code === coding.code)) {
426
+ // the code as given, then its normal form (see sourceCodes): an element written in the
427
+ // normal form matches a code given in another form, but not the other way round
428
+ const codes = this.sourceCodes(coding);
429
+ const elements = codes.flatMap(c => (g.element || []).filter(e => e.code === c));
430
+ for (const em of elements) {
423
431
  // ConceptMap.group.element.noMap says, positively, that this concept has no
424
432
  // mapping in this map - which is an answer, not the absence of one. Report it
425
433
  // as a match that states the source concept and the map it came from, with no
@@ -871,11 +879,19 @@ class TranslateWorker extends TerminologyWorker {
871
879
 
872
880
  let translations = await prov.getTranslations(cm, coding, target, reverse);
873
881
 
882
+ // The code system can say why there is no translation (an extension concept whose only
883
+ // International ancestor is a top-level hierarchy, say): that comes back as an entry
884
+ // with no code, and goes out as the message in place of the generic one
885
+ const explanations = translations.filter(t => !t.code && t.message);
886
+ translations = translations.filter(t => t.code);
887
+
874
888
  if (translations.length > 0) {
875
889
  result = true;
890
+ const usedMaps = new Set();
876
891
 
877
892
  for (const t of translations) {
878
- if (t.map) {
893
+ if (t.map && !usedMaps.has(t.map)) {
894
+ usedMaps.add(t.map);
879
895
  output.push({
880
896
  name: 'used-conceptmap',
881
897
  valueUri: t.map
@@ -889,6 +905,15 @@ class TranslateWorker extends TerminologyWorker {
889
905
  display: t.display
890
906
  };
891
907
 
908
+ const sourceConcept = {
909
+ system: coding.system,
910
+ code: coding.code
911
+ };
912
+ if (coding.version) {
913
+ // as given: the version says which edition the code came from, which is
914
+ // the point of an edition to edition translation
915
+ sourceConcept.version = coding.version;
916
+ }
892
917
  const matchParts = [];
893
918
  matchParts.push({
894
919
  name: 'concept',
@@ -896,10 +921,7 @@ class TranslateWorker extends TerminologyWorker {
896
921
  });
897
922
  matchParts.push({
898
923
  name: 'sourceConcept',
899
- valueCoding: {
900
- system: coding.system,
901
- code: coding.code
902
- }
924
+ valueCoding: sourceConcept
903
925
  });
904
926
  matchParts.push({
905
927
  name: 'relationship',
@@ -911,13 +933,26 @@ class TranslateWorker extends TerminologyWorker {
911
933
  valueString: t.message
912
934
  });
913
935
  }
914
- matchParts.push({
915
- name: 'originMap',
916
- valueCanonical: cm.vurl
917
- });
936
+ if (cm.url) {
937
+ matchParts.push({
938
+ name: 'originMap',
939
+ valueCanonical: cm.vurl
940
+ });
941
+ }
942
+ // the code system produced this target itself, from its own content (and for an
943
+ // edition to edition translation that content may be a version the server does not
944
+ // hold separately), so there is nothing to check it against: see checkTargetConcepts
918
945
  output.push({
919
946
  name: 'match',
920
- part: matchParts
947
+ part: matchParts,
948
+ [TRUSTED_TARGET]: true
949
+ });
950
+ }
951
+ } else {
952
+ for (const e of explanations) {
953
+ output.push({
954
+ name: 'message',
955
+ valueString: e.message
921
956
  });
922
957
  }
923
958
  }
@@ -1075,6 +1110,9 @@ class TranslateWorker extends TerminologyWorker {
1075
1110
  // across groups - one group having mapped the concept does not stop another group
1076
1111
  // from saying what it does with it.
1077
1112
  const maps = this.sortConceptMaps(conceptMaps);
1113
+ if (!reverse) {
1114
+ await this.noteSourceNormalForm(coding, params);
1115
+ }
1078
1116
  let added = false;
1079
1117
  for (const cm of maps) {
1080
1118
  if (cm.jsonObj.internalSource) {
@@ -1089,12 +1127,13 @@ class TranslateWorker extends TerminologyWorker {
1089
1127
  // or R4's unmatched/disjoint). That is still reported as a match - it is what the map
1090
1128
  // says - but it is not a translation: "The value can only be true if at least one
1091
1129
  // returned match has a relationship other than 'not-related-to'" (tx-ecosystem translate-2b)
1130
+ await this.checkTargetConcepts(result, params);
1092
1131
  const translated = added && result.some(p => p.name === 'match' && this.isPositiveMatch(p));
1093
1132
  result.push({
1094
1133
  name: 'result',
1095
1134
  valueBoolean: translated
1096
1135
  });
1097
- if (!added) {
1136
+ if (!added && !result.some(p => p.name === 'message')) {
1098
1137
  result.push({
1099
1138
  name: 'message',
1100
1139
  valueString: 'No translations found'
@@ -1125,6 +1164,118 @@ class TranslateWorker extends TerminologyWorker {
1125
1164
  };
1126
1165
  }
1127
1166
 
1167
+ /**
1168
+ * What the server can say about a code: whether its code system is known, whether the code
1169
+ * is valid there (undefined when the code system's content cannot tell - a fragment), and -
1170
+ * when it is - its normal form, as the code system has it (the same
1171
+ * normal form $validate-code reports as normalized-code) and its display. A code system
1172
+ * (or version) the server cannot resolve is not known.
1173
+ *
1174
+ * @returns {Promise<{known: boolean, valid?: boolean, normal?: string, display?: string}>}
1175
+ */
1176
+ async resolveCode(system, version, code, params) {
1177
+ if (!system || !code) {
1178
+ return { known: false };
1179
+ }
1180
+ this.deadCheck('translate-resolve');
1181
+ let cs;
1182
+ try {
1183
+ cs = await this.findCodeSystem(system, version || '', params, ['complete', 'fragment'], null, true);
1184
+ } catch (error) {
1185
+ if (error instanceof Issue) {
1186
+ return { known: false }; // an unknown version, say
1187
+ }
1188
+ throw error;
1189
+ }
1190
+ if (!cs) {
1191
+ return { known: false };
1192
+ }
1193
+ const loc = await cs.locate(code);
1194
+ if (!loc || !loc.context) {
1195
+ // a fragment, or a code system without its content, cannot say a code is not in it
1196
+ const mode = cs.contentMode ? String(cs.contentMode() || 'complete') : 'complete';
1197
+ return mode === 'complete' ? { known: true, valid: false } : { known: true };
1198
+ }
1199
+ const normal = await cs.code(loc.context);
1200
+ return { known: true, valid: true, normal: normal || code, display: await cs.display(loc.context) };
1201
+ }
1202
+
1203
+ /** The normal form of a code, with its display; null when it is the code as given, or unknown. */
1204
+ async normalForm(system, version, code, params) {
1205
+ const r = await this.resolveCode(system, version, code, params);
1206
+ if (!r.valid || r.normal === code) {
1207
+ return null;
1208
+ }
1209
+ return { code: r.normal, display: r.display };
1210
+ }
1211
+
1212
+ /**
1213
+ * The source code may not be in its code system's normal form (an ICD-11 expression written
1214
+ * with short codes, say). A map element written in the normal form still applies to it, so
1215
+ * translation looks for the code as given first, and then for its normal form. The map's own
1216
+ * codes are not normalised: an element written in some other form matches only that code.
1217
+ */
1218
+ async noteSourceNormalForm(coding, params) {
1219
+ const normal = await this.normalForm(coding.system, coding.version, coding.code, params);
1220
+ this.sourceNormal = normal ? { system: coding.system, code: coding.code, normal: normal.code } : null;
1221
+ }
1222
+
1223
+ /** The codes a map element may have for this source: as given, then the normal form. */
1224
+ sourceCodes(coding) {
1225
+ const n = this.sourceNormal;
1226
+ if (n && n.system === coding.system && n.code === coding.code) {
1227
+ return [coding.code, n.normal];
1228
+ }
1229
+ return [coding.code];
1230
+ }
1231
+
1232
+ /**
1233
+ * The target concepts the maps produced are checked against their code systems:
1234
+ *
1235
+ * - a target in a code system the server does not know gets a `note` part in its match,
1236
+ * saying that the code has not been validated
1237
+ * - a target that is not valid in its code system is still reported as the map has it, and
1238
+ * the response gets an `error` parameter for it (one per invalid target)
1239
+ * - a valid target that is not in its code system's normal form - an ICD-11 map written with
1240
+ * short codes, where the normal form is the entity id, say - also carries
1241
+ * `normalized-concept`: the same concept, as the code system has it
1242
+ *
1243
+ * This applies to any code system. `concept` is always the target as the map states it.
1244
+ */
1245
+ async checkTargetConcepts(result, params) {
1246
+ const errors = [];
1247
+ for (const match of result) {
1248
+ if (match.name !== 'match' || !Array.isArray(match.part) || match[TRUSTED_TARGET]) {
1249
+ continue;
1250
+ }
1251
+ const concept = match.part.find(p => p.name === 'concept')?.valueCoding;
1252
+ if (!concept || !concept.system || !concept.code) {
1253
+ continue;
1254
+ }
1255
+ const r = await this.resolveCode(concept.system, concept.version, concept.code, params);
1256
+ const at = match.part.findIndex(p => p.name === 'concept');
1257
+ const csName = concept.version ? `${concept.system}|${concept.version}` : concept.system;
1258
+ if (!r.known) {
1259
+ match.part.splice(at + 1, 0, { name: 'note', valueString:
1260
+ this.i18n.translate('TRANSLATE_TARGET_CS_UNKNOWN', params.FHTTPLanguages, [csName, concept.code]) });
1261
+ } else if (r.valid === false) {
1262
+ errors.push({ name: 'error', valueString:
1263
+ this.i18n.translate('CONCEPTMAP_GROUP_TARGET_CODE_INVALID', params.FHTTPLanguages, [concept.code, csName]) });
1264
+ } else if (r.valid && r.normal !== concept.code) {
1265
+ const coding = { system: concept.system };
1266
+ if (concept.version) {
1267
+ coding.version = concept.version;
1268
+ }
1269
+ coding.code = r.normal;
1270
+ if (r.display) {
1271
+ coding.display = r.display;
1272
+ }
1273
+ match.part.splice(at + 1, 0, { name: 'normalized-concept', valueCoding: coding });
1274
+ }
1275
+ }
1276
+ result.push(...errors);
1277
+ }
1278
+
1128
1279
  /**
1129
1280
  * Whether a match counts towards result=true. A noMap match says there is no mapping, and
1130
1281
  * an explicit negative relationship says the target is not related, so neither is a
@@ -582,6 +582,7 @@ class ValueSetChecker {
582
582
  messages.push(this.addNormalisationIssue(op, path, code, normalised, cs));
583
583
  normalForm.value = normalised;
584
584
  }
585
+ await this.noteClassificationCode(cs, ctxt.context, code, normalForm);
585
586
  let msg = await cs.incompleteValidationMessage(ctxt.context, this.params.HTTPLanguages);
586
587
  if (msg) {
587
588
  op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note'));
@@ -1079,6 +1080,8 @@ class ValueSetChecker {
1079
1080
  } else if (c.display && list.designations.length > 0) {
1080
1081
  await this.checkDisplays(list, defLang, c, msg, op, path);
1081
1082
  }
1083
+ await this.checkCodingExtensions(csd, c, path, op, msg);
1084
+ await this.checkCodeIssues(csd, c, path, op, msg);
1082
1085
  // Report the FIRST matching coding, not the last: when a CodeableConcept
1083
1086
  // has several valid codings, the code/system/version/display echoed back
1084
1087
  // are those of the earliest coding that validated. Only capture once
@@ -1349,10 +1352,18 @@ class ValueSetChecker {
1349
1352
  result.addParamCode('normalized-code', normalForm.value);
1350
1353
  }
1351
1354
 
1355
+ let outCode = null;
1352
1356
  if (pcode) {
1353
- result.addParamCode('code', pcode);
1357
+ outCode = pcode;
1354
1358
  } else if (tcode && mode !== 'codeableConcept') {
1355
- result.addParamCode('code', tcode);
1359
+ outCode = tcode;
1360
+ }
1361
+ if (outCode) {
1362
+ result.addParamCode('code', outCode);
1363
+ const cc = normalForm.classificationCodes?.get(outCode);
1364
+ if (cc) {
1365
+ result.addParamCode('classificationCode', cc);
1366
+ }
1356
1367
  }
1357
1368
  if (pver) {
1358
1369
  result.addParamStr('version', pver);
@@ -1416,6 +1427,46 @@ class ValueSetChecker {
1416
1427
  return result;
1417
1428
  }
1418
1429
 
1430
+ /**
1431
+ * Extensions on the coding that its code system defines and so can check (ICD-11's
1432
+ * classificationCode). Anything wrong is an error: the extension is there to be relied on.
1433
+ */
1434
+ async checkCodingExtensions(cs, c, path, op, msg) {
1435
+ if (!cs || typeof cs.checkCodingExtensions !== 'function' || !c.extension || c.extension.length === 0) {
1436
+ return;
1437
+ }
1438
+ const loc = await cs.locate(c.code);
1439
+ if (!loc.context) {
1440
+ return;
1441
+ }
1442
+ for (const p of await cs.checkCodingExtensions(c, loc.context)) {
1443
+ const m = this.worker.i18n.translate(p.msgId, this.params.HTTPLanguages, p.params);
1444
+ msg(m);
1445
+ op.addIssue(new Issue('error', 'invalid', addToPath(path, 'extension[' + p.index + ']'), p.msgId, m, 'invalid-data'));
1446
+ }
1447
+ }
1448
+
1449
+ /**
1450
+ * What the code system has to say about a code that is valid (an ICD-11 expression with two
1451
+ * values on an axis that takes one, say).
1452
+ */
1453
+ async checkCodeIssues(cs, c, path, op, msg) {
1454
+ if (!cs || typeof cs.codeIssues !== 'function' || !c.code) {
1455
+ return;
1456
+ }
1457
+ const loc = await cs.locate(c.code);
1458
+ if (!loc.context) {
1459
+ return;
1460
+ }
1461
+ for (const p of await cs.codeIssues(loc.context)) {
1462
+ const m = this.worker.i18n.translate(p.msgId, this.params.HTTPLanguages, p.params);
1463
+ if (p.severity === 'error') {
1464
+ msg(m);
1465
+ }
1466
+ op.addIssue(new Issue(p.severity, 'business-rule', addToPath(path, 'code'), p.msgId, m, 'code-rule'));
1467
+ }
1468
+ }
1469
+
1419
1470
  async checkDisplays(list, defLang, c, msg, op, path) {
1420
1471
  let hd = list.hasDisplay(this.params.workingLanguages(), null, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE)
1421
1472
  if (!hd.found) {
@@ -1522,6 +1573,60 @@ class ValueSetChecker {
1522
1573
  *
1523
1574
  * @returns {string} the message, for callers that also collect it into `messages`
1524
1575
  */
1576
+ /**
1577
+ * The code came in its normal form; if the code system also has a classification code for
1578
+ * it (ICD-11's short code), remember it against that code, so the output can report the
1579
+ * one that belongs to whatever ends up in the `code` parameter.
1580
+ */
1581
+ /**
1582
+ * For value set membership found through an enumerated concept or a filter: the same
1583
+ * normal-form reporting the whole-code-system path does, but only for code systems that
1584
+ * have a classification code (ICD-11), so that other code systems' value set results are
1585
+ * unchanged.
1586
+ */
1587
+ async noteValueSetNormalForm(op, path, cs, context, code, normalForm) {
1588
+ if (!(await cs.classificationCode(context))) {
1589
+ return;
1590
+ }
1591
+ const normalised = await cs.code(context);
1592
+ if (normalised && normalised !== code) {
1593
+ this.addNormalisationIssue(op, path, code, normalised, cs);
1594
+ normalForm.value = normalised;
1595
+ }
1596
+ await this.noteClassificationCode(cs, context, code, normalForm);
1597
+ }
1598
+
1599
+ /**
1600
+ * True when an enumerated concept in a value set denotes the same concept as the code
1601
+ * asked about, though spelled differently - ICD-11's short code and entity id. Only
1602
+ * code systems with a classification code have two spellings, so for the rest this is
1603
+ * one call and false.
1604
+ */
1605
+ async sameConceptDifferentForm(cs, code, enumerated) {
1606
+ if (code === enumerated) {
1607
+ return false;
1608
+ }
1609
+ const a = await cs.locate(code);
1610
+ if (!a.context || !(await cs.classificationCode(a.context))) {
1611
+ return false;
1612
+ }
1613
+ const b = await cs.locate(enumerated);
1614
+ return !!b.context && (await cs.code(a.context)) === (await cs.code(b.context));
1615
+ }
1616
+
1617
+ async noteClassificationCode(cs, context, code, normalForm) {
1618
+ // the code system decides whether this spelling of the code earns a classificationCode
1619
+ // (ICD-11: when it was written with entity ids, in any order - not when it was written
1620
+ // with the classification codes themselves)
1621
+ const cc = await cs.reportedClassificationCode(context, code);
1622
+ if (cc && cc !== code) {
1623
+ if (!normalForm.classificationCodes) {
1624
+ normalForm.classificationCodes = new Map();
1625
+ }
1626
+ normalForm.classificationCodes.set(code, cc);
1627
+ }
1628
+ }
1629
+
1525
1630
  addNormalisationIssue(op, path, code, normalised, cs) {
1526
1631
  const caseOnly = normalised.toLowerCase() === code.toLowerCase();
1527
1632
  const id = caseOnly ? 'CODE_CASE_DIFFERENCE' : 'CODE_NOT_IN_NORMAL_FORM';
@@ -1558,6 +1663,7 @@ class ValueSetChecker {
1558
1663
  this.addNormalisationIssue(op, path, code, normalised, cs);
1559
1664
  normalForm.value = normalised;
1560
1665
  }
1666
+ await this.noteClassificationCode(cs, loc.context, code, normalForm);
1561
1667
  let msg = await cs.incompleteValidationMessage(loc.context, this.params.HTTPLanguages);
1562
1668
  if (msg) {
1563
1669
  op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note'));
@@ -1623,10 +1729,20 @@ class ValueSetChecker {
1623
1729
  }
1624
1730
  }
1625
1731
 
1732
+ // an enumerated concept may be written in the other form (ICD-11 short code vs uri)
1733
+ let altMatch = null;
1734
+ if (cset.concept && !cset.concept.some(cc => cc.code === code)) {
1735
+ for (const cc of cset.concept) {
1736
+ if (await this.sameConceptDifferentForm(cs, code, cc.code)) {
1737
+ altMatch = cc;
1738
+ break;
1739
+ }
1740
+ }
1741
+ }
1626
1742
  for (let cc of cset.concept || []) {
1627
1743
  this.worker.deadCheck('checkConceptSet#1');
1628
1744
  let c = cc.code;
1629
- if (code === c) {
1745
+ if (code === c || cc === altMatch) {
1630
1746
  let loc = await cs.locate(code);
1631
1747
  if (loc.context !== null) {
1632
1748
  loc = loc.context;
@@ -1662,6 +1778,9 @@ class ValueSetChecker {
1662
1778
  inactive.code = code;
1663
1779
  vstatus.value = await cs.getStatus(loc);
1664
1780
  }
1781
+ if (role !== 'not in') {
1782
+ await this.noteValueSetNormalForm(op, path, cs, loc, code, normalForm);
1783
+ }
1665
1784
  result = true;
1666
1785
  return result;
1667
1786
  }
@@ -1691,7 +1810,7 @@ class ValueSetChecker {
1691
1810
  let loc = await cs.filterLocate(prep, filters[0], code);
1692
1811
  if (loc != null && !(typeof loc === 'string') && filters.length > 1) {
1693
1812
  for (let i = 1; i < filters.length; i++) {
1694
- if (!(await cs.filterCheck(prep, filters[i], loc))) {
1813
+ if ((await cs.filterCheck(prep, filters[i], loc)) != true) { // a failing check may return a message, which is truthy
1695
1814
  loc = null;
1696
1815
  break;
1697
1816
  }
@@ -1733,6 +1852,9 @@ class ValueSetChecker {
1733
1852
  }
1734
1853
  }
1735
1854
  }
1855
+ if (role !== 'not in') {
1856
+ await this.noteValueSetNormalForm(op, path, cs, loc, code, normalForm);
1857
+ }
1736
1858
  result = true;
1737
1859
  return result;
1738
1860
  }
@@ -2035,7 +2157,7 @@ class ValidateWorker extends TerminologyWorker {
2035
2157
  return res.status(422).json(this.operationOutcome('error', 'not-found',
2036
2158
  `CodeSystem/${id} not found`));
2037
2159
  }
2038
- const csp = new FhirCodeSystemProvider(this.opContext, new CodeSystem(codeSystem), []);
2160
+ const csp = FhirCodeSystemProvider.create(this.opContext, new CodeSystem(codeSystem), []);
2039
2161
 
2040
2162
  // Extract coded value
2041
2163
  let mode = { mode : null }
@@ -2168,7 +2290,7 @@ class ValidateWorker extends TerminologyWorker {
2168
2290
  // Check for codeSystem resource parameter
2169
2291
  const csResource = this.getResourceParam(params, 'codeSystem');
2170
2292
  if (csResource) {
2171
- return new FhirCodeSystemProvider(this.opContext, new CodeSystem(csResource), []); // todo: supplements
2293
+ return FhirCodeSystemProvider.create(this.opContext, new CodeSystem(csResource), []); // todo: supplements
2172
2294
  }
2173
2295
  let path = coded == null ? null : this.systemPath(mode);
2174
2296
  let fromCoded = false;