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.
- package/CHANGELOG.md +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/readme.md +1 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
package/tx/workers/translate.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
package/tx/workers/validate.js
CHANGED
|
@@ -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
|
-
|
|
1357
|
+
outCode = pcode;
|
|
1354
1358
|
} else if (tcode && mode !== 'codeableConcept') {
|
|
1355
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
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;
|