fhirsmith 0.18.0 → 0.18.2
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 +72 -10
- package/ips/ips.js +251 -26
- package/ips/jurisdictions.js +65 -0
- package/ips/pipeline.js +67 -6
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +48 -5
- package/ips/render.js +317 -46
- package/ips/scripts.js +46 -0
- package/ips/store.js +55 -0
- package/library/daily-report.js +25 -15
- package/package.json +3 -3
- package/registry/api.js +179 -1
- package/registry/crawler.js +2 -0
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry.js +53 -2
- package/translations/Messages.properties +1 -0
- package/tx/README.md +4 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cm/cm-package.js +21 -15
- package/tx/cs/cs-api.js +3 -1
- package/tx/cs/cs-cs.js +59 -12
- package/tx/cs/cs-icf.js +47 -34
- package/tx/cs/cs-loinc.js +257 -3
- package/tx/cs/cs-snomed.js +544 -55
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +131 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library/tho-precedence.js +40 -0
- package/tx/openapi-operations.js +2 -1
- package/tx/provider.js +46 -29
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +34 -11
- package/tx/sct/structures.js +15 -2
- package/tx/tx.fhir.org.yml +2 -2
- package/tx/vs/vs-package.js +17 -13
- package/tx/vs/vs-vsac.js +1 -1
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +2 -2
- package/tx/workers/validate.js +10 -0
- package/utilities/make-sct-loinc-subset.js +250 -0
|
@@ -564,6 +564,19 @@ class Designations {
|
|
|
564
564
|
return this._langsMatch(langList, cd.language, LangMatchType.LANG, defLang);
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Note, in the set of supplements to report as used, the supplement a designation came from (if
|
|
569
|
+
* it came from one). Designations from a supplement carry it as .supplement (see
|
|
570
|
+
* CodeSystemProvider._listSupplementDesignations); this used to look for .source, which is never
|
|
571
|
+
* set on a designation, so a supplement that only supplied the chosen display - a global language
|
|
572
|
+
* pack, typically - was never reported as used.
|
|
573
|
+
*/
|
|
574
|
+
_reportSupplement(supplements, cd) {
|
|
575
|
+
if (supplements && cd && cd.supplement && cd.supplement.vurl) {
|
|
576
|
+
supplements.add(cd.supplement.vurl);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
567
580
|
/**
|
|
568
581
|
* Find the preferred designation for given language preferences
|
|
569
582
|
*/
|
|
@@ -576,39 +589,29 @@ class Designations {
|
|
|
576
589
|
// No language list, prefer base designations
|
|
577
590
|
for (const cd of this.designations) {
|
|
578
591
|
if (this._isPreferred(cd) && cd.isActive()) {
|
|
579
|
-
|
|
580
|
-
supplements.add(cd.source);
|
|
581
|
-
}
|
|
592
|
+
this._reportSupplement(supplements, cd);
|
|
582
593
|
return cd;
|
|
583
594
|
}
|
|
584
595
|
}
|
|
585
596
|
for (const cd of this.designations) {
|
|
586
597
|
if (this.isDisplay(cd) && cd.isActive()) {
|
|
587
|
-
|
|
588
|
-
supplements.add(cd.source);
|
|
589
|
-
}
|
|
598
|
+
this._reportSupplement(supplements, cd);
|
|
590
599
|
return cd;
|
|
591
600
|
}
|
|
592
601
|
}
|
|
593
602
|
for (const cd of this.designations) {
|
|
594
603
|
if (this.isDisplay(cd)) {
|
|
595
|
-
|
|
596
|
-
supplements.add(cd.source);
|
|
597
|
-
}
|
|
604
|
+
this._reportSupplement(supplements, cd);
|
|
598
605
|
return cd;
|
|
599
606
|
}
|
|
600
607
|
}
|
|
601
608
|
for (const cd of this.designations) {
|
|
602
609
|
if (this._isPreferred(cd)) {
|
|
603
|
-
|
|
604
|
-
supplements.add(cd.source);
|
|
605
|
-
}
|
|
610
|
+
this._reportSupplement(supplements, cd);
|
|
606
611
|
return cd;
|
|
607
612
|
}
|
|
608
613
|
}
|
|
609
|
-
|
|
610
|
-
supplements.add(this.designations[0].source);
|
|
611
|
-
}
|
|
614
|
+
this._reportSupplement(supplements, this.designations[0]);
|
|
612
615
|
return this.designations[0];
|
|
613
616
|
}
|
|
614
617
|
|
|
@@ -628,25 +631,19 @@ class Designations {
|
|
|
628
631
|
// because the FSN was emitted first.
|
|
629
632
|
for (const cd of this.designations) {
|
|
630
633
|
if (this._langMatches(lang, cd.language, matchType) && this._isPreferred(cd) && (!activeOnly || cd.isActive())) {
|
|
631
|
-
|
|
632
|
-
supplements.add(cd.source);
|
|
633
|
-
}
|
|
634
|
+
this._reportSupplement(supplements, cd);
|
|
634
635
|
return cd;
|
|
635
636
|
}
|
|
636
637
|
}
|
|
637
638
|
for (const cd of this.designations) {
|
|
638
639
|
if (this._langMatches(lang, cd.language, matchType) && this.isDisplay(cd) && (!activeOnly || cd.isActive())) {
|
|
639
|
-
|
|
640
|
-
supplements.add(cd.source);
|
|
641
|
-
}
|
|
640
|
+
this._reportSupplement(supplements, cd);
|
|
642
641
|
return cd;
|
|
643
642
|
}
|
|
644
643
|
}
|
|
645
644
|
for (const cd of this.designations) {
|
|
646
645
|
if (this._langMatches(lang, cd.language, matchType) && (!activeOnly || cd.isActive())) {
|
|
647
|
-
|
|
648
|
-
supplements.add(cd.source);
|
|
649
|
-
}
|
|
646
|
+
this._reportSupplement(supplements, cd);
|
|
650
647
|
return cd;
|
|
651
648
|
}
|
|
652
649
|
}
|
|
@@ -655,14 +652,10 @@ class Designations {
|
|
|
655
652
|
}
|
|
656
653
|
for (const cd of this.designations) {
|
|
657
654
|
if (!cd.language && this.isDisplay(cd) && cd.isActive()) {
|
|
658
|
-
|
|
659
|
-
supplements.add(cd.source);
|
|
660
|
-
}
|
|
655
|
+
this._reportSupplement(supplements, cd);
|
|
661
656
|
return cd;
|
|
662
657
|
} if (!cd.language && this.isDisplay(cd)) {
|
|
663
|
-
|
|
664
|
-
supplements.add(cd.source);
|
|
665
|
-
}
|
|
658
|
+
this._reportSupplement(supplements, cd);
|
|
666
659
|
return cd;
|
|
667
660
|
}
|
|
668
661
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// THO precedence for value sets and concept maps (see Provider.applyTHOPrecedence).
|
|
3
|
+
//
|
|
4
|
+
// Where hl7.terminology provides a resource with the same url as one in the FHIR core
|
|
5
|
+
// package, THO's copy is the one used - versions were reset when content moved to THO, so
|
|
6
|
+
// version order can't decide. But the core copy is still what a reference that names its
|
|
7
|
+
// version (url|5.0.0) means, so it stays available to that reference, and only to it.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
const { VersionUtilities } = require('../../library/version-utilities');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether a package provider should keep a map key for a resource that THO also provides.
|
|
14
|
+
* Only the keys that name a version are kept - url|version, and url|major.minor for a
|
|
15
|
+
* semver version - and only where THO doesn't provide that version itself. The bare url
|
|
16
|
+
* (the default for a reference with no version) and the resource's id are not kept.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} key - the map key
|
|
19
|
+
* @param {{url: string, version: string}} resource - the core package's resource
|
|
20
|
+
* @param {Set<string>} thoVersions - the versions of the url that hl7.terminology provides
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function keepForPinnedReference(key, resource, thoVersions) {
|
|
24
|
+
const version = resource.version;
|
|
25
|
+
if (!version) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (key === resource.url + '|' + version) {
|
|
29
|
+
return !thoVersions.has(version);
|
|
30
|
+
}
|
|
31
|
+
if (VersionUtilities.isSemVer(version)) {
|
|
32
|
+
const mm = VersionUtilities.getMajMin(version);
|
|
33
|
+
if (mm && key === resource.url + '|' + mm) {
|
|
34
|
+
return ![...thoVersions].some(v => v && VersionUtilities.isSemVer(v) && VersionUtilities.getMajMin(v) === mm);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { keepForPinnedReference };
|
package/tx/openapi-operations.js
CHANGED
|
@@ -332,7 +332,8 @@ const OPERATIONS = [
|
|
|
332
332
|
p('name', 'string', '1', 'The closure table', { min: 1 }),
|
|
333
333
|
p('concept', 'Coding', '*', 'A concept to add'),
|
|
334
334
|
p('version', 'string', '1', 'Return the changes since this version of the table, instead of adding concepts'),
|
|
335
|
-
p('reset', 'boolean', '1', 'Start the table again')
|
|
335
|
+
p('reset', 'boolean', '1', 'Start the table again'),
|
|
336
|
+
p('dispose', 'boolean', '1', 'The client has finished with the table, and the server can delete it (not in the specification)')
|
|
336
337
|
],
|
|
337
338
|
returns: 'ConceptMap',
|
|
338
339
|
returnsDoc: 'The new relationships'
|
package/tx/provider.js
CHANGED
|
@@ -286,10 +286,13 @@ class Provider {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// Ask each code system version once: an implicit map can belong to one version
|
|
290
|
+
// only (a SNOMED CT edition's maps are named by its edition URI)
|
|
289
291
|
let uris = new Set();
|
|
290
292
|
for (let csp of this.codeSystemFactories.values()) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
const key = csp.system() + '|' + csp.version();
|
|
294
|
+
if (!uris.has(key)) {
|
|
295
|
+
uris.add(key);
|
|
293
296
|
let cm = await csp.findImplicitConceptMap(url, version);
|
|
294
297
|
if (cm) {
|
|
295
298
|
return cm;
|
|
@@ -343,16 +346,19 @@ class Provider {
|
|
|
343
346
|
// version (every SNOMED edition, say) is asked, since whether a map exists and what
|
|
344
347
|
// it says can depend on which version the source coding is in. The factory decides
|
|
345
348
|
// from sourceVersion whether it is the one being asked about.
|
|
349
|
+
// The same goes for the target system: a map from another system into one SNOMED
|
|
350
|
+
// edition (LOINC codes to the concepts that carry them, say) belongs to that edition.
|
|
351
|
+
const targetBase = targetSystem.split('|')[0];
|
|
346
352
|
let uris = new Set();
|
|
347
353
|
let asked = new Set(); // a factory is registered under several keys (system, system|version...)
|
|
348
354
|
for (let csp of this.codeSystemFactories.values()) {
|
|
349
355
|
if (asked.has(csp)) {
|
|
350
356
|
continue;
|
|
351
357
|
}
|
|
352
|
-
if (csp.system() === sourceSystem || !uris.has(csp.system())) {
|
|
358
|
+
if (csp.system() === sourceSystem || csp.system() === targetBase || !uris.has(csp.system())) {
|
|
353
359
|
uris.add(csp.system());
|
|
354
360
|
asked.add(csp);
|
|
355
|
-
await csp.findImplicitConceptMaps(conceptMaps, sourceSystem, sourceVersion, targetSystem);
|
|
361
|
+
await csp.findImplicitConceptMaps(conceptMaps, sourceSystem, sourceVersion, targetSystem, sourceCode);
|
|
356
362
|
}
|
|
357
363
|
}
|
|
358
364
|
}
|
|
@@ -626,27 +632,20 @@ class Provider {
|
|
|
626
632
|
* moved from the FHIR core packages to terminology.hl7.org has been QA'd there since, and
|
|
627
633
|
* its versions went backwards (coverage-selfpay is 4.0.1 in hl7.fhir.r4.core but 1.0.1 in
|
|
628
634
|
* hl7.terminology), so version precedence can't decide between them. Instead, a code system
|
|
629
|
-
* from an hl7.terminology package
|
|
630
|
-
*
|
|
635
|
+
* from an hl7.terminology package is always the default for its url - what a reference with
|
|
636
|
+
* no version gets - whatever versions the FHIR core packages have.
|
|
631
637
|
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
638
|
+
* The core versions stay available by url|version, where THO doesn't provide that version
|
|
639
|
+
* itself: a reference that names its version (url|5.0.0) means the core one, and there's
|
|
640
|
+
* nothing else it can mean. They are never the default while THO provides the url (see also
|
|
641
|
+
* deleteCodeSystem).
|
|
636
642
|
*/
|
|
637
643
|
addCodeSystem(cs) {
|
|
638
644
|
if (Provider.#isTHOPackage(cs.sourcePackage)) {
|
|
639
|
-
|
|
640
|
-
if (t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)
|
|
641
|
-
&& (key === t.url || !t.urlHadVersion)) {
|
|
642
|
-
this.codeSystems.delete(key);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
} else if (VersionUtilities.isCorePackage(cs.sourcePackage) && !cs.urlHadVersion) {
|
|
646
|
-
// THO's code system is always the default for its url
|
|
645
|
+
// THO's code system takes over the default from a core one; the core versions stay
|
|
647
646
|
const t = this.codeSystems.get(cs.url);
|
|
648
|
-
if (t &&
|
|
649
|
-
|
|
647
|
+
if (t && t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)) {
|
|
648
|
+
this.codeSystems.delete(cs.url);
|
|
650
649
|
}
|
|
651
650
|
}
|
|
652
651
|
const existing = this.codeSystems.get(cs.url);
|
|
@@ -671,45 +670,63 @@ class Provider {
|
|
|
671
670
|
* THO precedence for value sets and concept maps, as for code systems (see addCodeSystem):
|
|
672
671
|
* once the library's providers have been added after this endpoint's core package, the core
|
|
673
672
|
* package's value sets and concept maps whose urls an hl7.terminology package also provides
|
|
674
|
-
* are
|
|
673
|
+
* are only available to a reference that names their version (and THO doesn't provide that
|
|
674
|
+
* version itself). They are never what a reference with no version gets, and they aren't
|
|
675
|
+
* read by id, found by search, or used for translation unless asked for.
|
|
675
676
|
*/
|
|
676
677
|
applyTHOPrecedence() {
|
|
677
|
-
const thoValueSets = new
|
|
678
|
+
const thoValueSets = new Map();
|
|
678
679
|
for (const vp of this.valueSetProviders) {
|
|
679
680
|
if (vp instanceof PackageValueSetProvider && Provider.#isTHOPackage(vp.sourcePackage())) {
|
|
680
681
|
for (const vs of vp.valueSetMap.values()) {
|
|
681
|
-
thoValueSets
|
|
682
|
+
Provider.#addVersion(thoValueSets, vs);
|
|
682
683
|
}
|
|
683
684
|
}
|
|
684
685
|
}
|
|
685
|
-
const thoConceptMaps = new
|
|
686
|
+
const thoConceptMaps = new Map();
|
|
686
687
|
for (const cmp of this.conceptMapProviders) {
|
|
687
688
|
if (cmp instanceof PackageConceptMapProvider && Provider.#isTHOPackage(cmp.sourcePackage())) {
|
|
688
689
|
for (const cm of cmp.conceptMapMap.values()) {
|
|
689
|
-
thoConceptMaps
|
|
690
|
+
Provider.#addVersion(thoConceptMaps, cm);
|
|
690
691
|
}
|
|
691
692
|
}
|
|
692
693
|
}
|
|
693
694
|
for (const vp of this.valueSetProviders) {
|
|
694
695
|
if (vp instanceof PackageValueSetProvider && VersionUtilities.isCorePackage(vp.sourcePackage())) {
|
|
695
|
-
vp.
|
|
696
|
+
vp.keepPinnedOnly(thoValueSets);
|
|
696
697
|
}
|
|
697
698
|
}
|
|
698
699
|
for (const cmp of this.conceptMapProviders) {
|
|
699
700
|
if (cmp instanceof PackageConceptMapProvider && VersionUtilities.isCorePackage(cmp.sourcePackage())) {
|
|
700
|
-
cmp.
|
|
701
|
+
cmp.keepPinnedOnly(thoConceptMaps);
|
|
701
702
|
}
|
|
702
703
|
}
|
|
703
704
|
}
|
|
704
705
|
|
|
706
|
+
static #addVersion(map, resource) {
|
|
707
|
+
if (!resource || !resource.url) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (!map.has(resource.url)) {
|
|
711
|
+
map.set(resource.url, new Set());
|
|
712
|
+
}
|
|
713
|
+
if (resource.version) {
|
|
714
|
+
map.get(resource.url).add(resource.version);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
705
718
|
deleteCodeSystem(cs) {
|
|
706
719
|
this.codeSystems.delete(cs.vurl);
|
|
707
720
|
this.codeSystems.delete(cs.url);
|
|
708
721
|
// If other versions of the SAME url survive, re-point the unversioned [url]
|
|
709
722
|
// entry at the most-recent surviving version. Otherwise leave it deleted.
|
|
723
|
+
// THO precedence (see addCodeSystem): while a THO version survives, a core one
|
|
724
|
+
// can't become the default, however recent its (reset) version looks
|
|
725
|
+
const survivors = [...new Set(this.codeSystems.values())].filter(t => t.url === cs.url);
|
|
726
|
+
const tho = survivors.filter(t => Provider.#isTHOPackage(t.sourcePackage));
|
|
710
727
|
let existing = null;
|
|
711
|
-
for (let t of
|
|
712
|
-
if (
|
|
728
|
+
for (let t of (tho.length > 0 ? tho : survivors)) {
|
|
729
|
+
if (!existing || t.isMoreRecent(existing)) {
|
|
713
730
|
existing = t;
|
|
714
731
|
}
|
|
715
732
|
}
|
package/tx/sct/documentation.md
CHANGED
|
@@ -35,14 +35,20 @@ that the server does not use. `filterECL` is the one that matters.
|
|
|
35
35
|
reference set's own fields (concept, integer and string typed from the RF2
|
|
36
36
|
file name). Since inactive member rows are dropped at import, `active = false`
|
|
37
37
|
never matches.
|
|
38
|
-
-
|
|
38
|
+
- Refinements: `=` and `!=` against an expression value (`!=` counts the
|
|
39
|
+
occurrences whose value is outside the set, so `A != X` needs at least one
|
|
40
|
+
A that is not X, and `[0..0] A != X` means every A is X); reversed
|
|
41
|
+
attributes (`R A = X`: the concept is the value of an A on something in X,
|
|
42
|
+
evaluated over the concept's inbound relationships); attribute names that
|
|
43
|
+
are a concept, `*`, or any sub expression (`<< 762705008 = ...`); attribute
|
|
44
|
+
sets joined by `AND`, `OR` or `,`, with brackets; attribute groups,
|
|
45
|
+
including `AND`/`OR` inside and between groups; attribute and group
|
|
46
|
+
cardinality, including `[0..0]`. Cardinality counts occurrences per role
|
|
47
|
+
group, per the SNOMED Computable Languages Group ruling (see
|
|
48
|
+
`_countAttributeMatches`).
|
|
49
|
+
- `AND` / `OR` / `MINUS`, and `,` as a conjunction at the expression level as
|
|
50
|
+
well as in refinements, with the ECL 6.4 rule that conjunction and
|
|
39
51
|
disjunction cannot be mixed without brackets.
|
|
40
|
-
- Refinements: `=` against an expression value; attribute names that are a
|
|
41
|
-
concept, `*`, or any sub expression (`<< 762705008 = ...`); attribute sets
|
|
42
|
-
joined by `AND`, `OR` or `,`, with brackets; attribute groups, including
|
|
43
|
-
`AND`/`OR` inside and between groups; attribute and group cardinality,
|
|
44
|
-
including `[0..0]`. Cardinality counts occurrences per role group, per the
|
|
45
|
-
SNOMED Computable Languages Group ruling (see `_countAttributeMatches`).
|
|
46
52
|
- Dotted expressions (`< X . 363698007`, `. << 363698007`, `. (A OR B)`).
|
|
47
53
|
- `{{ C ... }}` concept filters on `active`, `definitionStatus`,
|
|
48
54
|
`definitionStatusId`, `moduleId` and `effectiveTime`.
|
|
@@ -56,26 +62,37 @@ that the server does not use. `filterECL` is the one that matters.
|
|
|
56
62
|
must be satisfied by a single description; separate blocks may be satisfied
|
|
57
63
|
by different descriptions. Only active descriptions are considered unless
|
|
58
64
|
the block has an `active` filter.
|
|
65
|
+
- Concrete values in refinements - numeric (`= #5`, `>= #1.5`, `!= #-1`),
|
|
66
|
+
string (`= "x"`, `= match:"x"`, a set) and boolean (`= true`) - against the
|
|
67
|
+
concrete value relationships, which the importer reads from
|
|
68
|
+
`sct2_RelationshipConcreteValues_*` into cache version 19. Numbers compare
|
|
69
|
+
as numbers (`#5` matches `#5.0`); strings use the same match / wild
|
|
70
|
+
semantics as description terms; a value of another kind never matches.
|
|
71
|
+
An edition imported before version 19 has no concrete values and reports
|
|
72
|
+
such a refinement as unsupported rather than answering with nothing.
|
|
73
|
+
- History supplements: `{{ + HISTORY }}` (= MAX), `HISTORY-MIN` (SAME AS),
|
|
74
|
+
`HISTORY-MOD` (SAME AS, WAS A, REPLACED BY, PARTIALLY EQUIVALENT TO),
|
|
75
|
+
`HISTORY-MAX` (every reference set under 900000000000522004 |Historical
|
|
76
|
+
association|), `HISTORY (*)` and `HISTORY (expression)`: the inactive
|
|
77
|
+
concepts that the selected association reference sets associate with a
|
|
78
|
+
concept in the result are added to it. A reference set the edition does not
|
|
79
|
+
carry contributes nothing. Whether a bare `HISTORY` means MAX is taken from
|
|
80
|
+
Snow Owl's reading of the specification - to be confirmed.
|
|
81
|
+
- Alternate identifiers (`LOINC#14394-1`, `"LOINC#14394-1"`): the scheme
|
|
82
|
+
alias names an identifier scheme (`IDENTIFIER_SCHEMES` in `cs-snomed.js`:
|
|
83
|
+
LOINC = 30051010000102, from the LOINC Extension's `sct2_Identifier` file,
|
|
84
|
+
cache version 18+), the code resolves to the concept(s) carrying it, and
|
|
85
|
+
hierarchy operators apply to them as to any set. An unknown scheme or a
|
|
86
|
+
code nobody carries is `INVALID_ECL`.
|
|
59
87
|
- Comments (`/* ... */`), terms (`|...|`, not checked against descriptions).
|
|
60
88
|
|
|
61
89
|
### Parsed but reported as unsupported
|
|
62
90
|
|
|
63
|
-
- Reverse attributes (`R 363698007 = ...`).
|
|
64
|
-
- `!=` against an expression value in a refinement.
|
|
65
|
-
- Concrete values in refinements - numeric (`= #5`, `>= #1.5`), string
|
|
66
|
-
(`= "x"`, `= match:"x"`), boolean (`= true`). The importer does not read
|
|
67
|
-
`sct2_RelationshipConcreteValues_*`, so no concrete values are in the cache
|
|
68
|
-
to compare against. Supporting them means extending the importer and the
|
|
69
|
-
relationship record, and rebuilding every cache.
|
|
70
|
-
- Alternate identifiers (`"ICD10#A01.1"`, `SCTID#123`). No alternate
|
|
71
|
-
identifier reference sets are loaded.
|
|
72
91
|
- Reference set field selection on member-of (`^ [targetComponentId] refset`,
|
|
73
92
|
`^ [referencedComponentId, targetComponentId] refset`). Deliberate: the
|
|
74
93
|
selected fields, and the referenced components of many reference sets,
|
|
75
94
|
are descriptions or other non-concept components, which an expansion has
|
|
76
95
|
no way to represent.
|
|
77
|
-
- History supplements (`{{ + HISTORY-MIN / MOD / MAX }}`). The association
|
|
78
|
-
reference sets they draw on are loaded, so this is implementable.
|
|
79
96
|
|
|
80
97
|
### Not parsed
|
|
81
98
|
|
|
@@ -99,7 +116,13 @@ that the server does not use. `filterECL` is the one that matters.
|
|
|
99
116
|
- `^` on a concept that is not a reference set yields an empty result rather
|
|
100
117
|
than an error (Snowstorm's behaviour; agreed in the tx-ecosystem suite).
|
|
101
118
|
`^` on a reference set whose members are descriptions (a language reference
|
|
102
|
-
set) is reported as unsupported
|
|
119
|
+
set) is reported as unsupported when that reference set is named explicitly
|
|
120
|
+
- there are no concepts to return. Inside a computed operand (`^ *`,
|
|
121
|
+
`^ (< 900000000000455006)`) such a reference set contributes nothing and
|
|
122
|
+
the rest of the operand still counts.
|
|
123
|
+
- An attribute name without a comparison (`< X : 363698007`, or `(< X : A = v
|
|
124
|
+
OR B)` where the OR binds inside the refinement) is a syntax error, as the
|
|
125
|
+
grammar requires.
|
|
103
126
|
- The ECL result for a value set filter is cached per (value set, code system
|
|
104
127
|
version), so the first expansion of a large constraint is slow and later ones
|
|
105
128
|
are not.
|
|
@@ -181,8 +204,9 @@ role group pairing.
|
|
|
181
204
|
|
|
182
205
|
- Stated relationships and the OWL axiom reference sets. Everything is driven
|
|
183
206
|
by the distributed inferred relationships; there is no classification.
|
|
184
|
-
- Concrete value relationships (
|
|
185
|
-
-
|
|
207
|
+
- Concrete value relationships are read into cache version 19 (October 2026);
|
|
208
|
+
caches built before that have none, and a concrete-value ECL refinement on
|
|
209
|
+
them is reported as unsupported.
|
|
186
210
|
- Inactive reference set member rows (only active rows are stored).
|
|
187
211
|
- Module ids and definition status on concepts are recorded by the current
|
|
188
212
|
importer; caches built before mid-2026 have neither (every concept looks
|
package/tx/sct/ecl.js
CHANGED
|
@@ -538,8 +538,11 @@ class ECLParser {
|
|
|
538
538
|
// this compound sequence and reject a mix (e.g. "<< A OR << B AND << C").
|
|
539
539
|
let compoundOp = null;
|
|
540
540
|
|
|
541
|
-
|
|
542
|
-
|
|
541
|
+
// "," is a conjunction in the ECL grammar (conjunction = "AND" / ","), at
|
|
542
|
+
// this level as well as in refinements; a comma that reaches here was not
|
|
543
|
+
// claimed by a refinement.
|
|
544
|
+
while (this.match(ECLTokenType.AND, ECLTokenType.OR, ECLTokenType.MINUS, ECLTokenType.COMMA)) {
|
|
545
|
+
const operator = this.match(ECLTokenType.COMMA) ? { ...this.current, type: ECLTokenType.AND } : this.current;
|
|
543
546
|
if (compoundOp !== null && operator.type !== compoundOp) {
|
|
544
547
|
throw new Error(
|
|
545
548
|
'Ambiguous ECL expression: conjunction, disjunction and exclusion ' +
|
|
@@ -726,18 +729,30 @@ class ECLParser {
|
|
|
726
729
|
return { type: ECLNodeType.FILTER_CONSTRAINT, kind, filters };
|
|
727
730
|
}
|
|
728
731
|
|
|
729
|
-
if (kind
|
|
730
|
-
//
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
732
|
+
if (kind === '+') {
|
|
733
|
+
// historySupplement = "{{" "+" "HISTORY" [ "-MIN" / "-MOD" / "-MAX" / "(" (expressionConstraint / "*") ")" ] "}}"
|
|
734
|
+
if (!this.match(ECLTokenType.SCTID) || !/^HISTORY(-MIN|-MOD|-MAX)?$/i.test(this.current.value)) {
|
|
735
|
+
this.error('Expected HISTORY, HISTORY-MIN, HISTORY-MOD or HISTORY-MAX after {{ +');
|
|
736
|
+
}
|
|
737
|
+
const word = this.current.value.toUpperCase();
|
|
738
|
+
this.advance();
|
|
739
|
+
const node = { type: ECLNodeType.FILTER_CONSTRAINT, kind, profile: word === 'HISTORY' ? 'MAX' : word.substring(8) };
|
|
740
|
+
if (this.match(ECLTokenType.LPAREN)) {
|
|
741
|
+
if (node.profile !== 'MAX' || word !== 'HISTORY') {
|
|
742
|
+
this.error('A history profile (HISTORY-MIN/MOD/MAX) cannot also name reference sets');
|
|
735
743
|
}
|
|
736
|
-
raw.push(this.current.value);
|
|
737
744
|
this.advance();
|
|
745
|
+
if (this.match(ECLTokenType.WILDCARD)) {
|
|
746
|
+
this.advance();
|
|
747
|
+
node.profile = 'MAX';
|
|
748
|
+
} else {
|
|
749
|
+
node.profile = 'EXPLICIT';
|
|
750
|
+
node.refsets = this.parseExpressionConstraint();
|
|
751
|
+
}
|
|
752
|
+
this.expect(ECLTokenType.RPAREN);
|
|
738
753
|
}
|
|
739
|
-
this.
|
|
740
|
-
return
|
|
754
|
+
this.expect(ECLTokenType.RFILTER);
|
|
755
|
+
return node;
|
|
741
756
|
}
|
|
742
757
|
|
|
743
758
|
const filters = [this.parseMemberFilter()];
|
|
@@ -1430,6 +1445,14 @@ class ECLParser {
|
|
|
1430
1445
|
};
|
|
1431
1446
|
}
|
|
1432
1447
|
|
|
1448
|
+
if (!comparison) {
|
|
1449
|
+
// eclAttribute = [cardinality] [reverseFlag] eclAttributeName comparison - a
|
|
1450
|
+
// bare name is a syntax error. This is what "(A : B = C OR D)" trips on:
|
|
1451
|
+
// the OR binds inside the refinement, so D is an attribute name without a
|
|
1452
|
+
// comparison.
|
|
1453
|
+
this.error('Expected a comparison operator (=, !=, <, <=, >, >=) after the attribute name');
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1433
1456
|
return {
|
|
1434
1457
|
type: ECLNodeType.ATTRIBUTE,
|
|
1435
1458
|
cardinality,
|
package/tx/sct/structures.js
CHANGED
|
@@ -1358,7 +1358,7 @@ class SnomedFileReader {
|
|
|
1358
1358
|
}
|
|
1359
1359
|
|
|
1360
1360
|
// Determine settings based on cache version string (matching Pascal logic)
|
|
1361
|
-
if (result.cacheVersion === '17') { //
|
|
1361
|
+
if (result.cacheVersion === '17' || result.cacheVersion === '18' || result.cacheVersion === '19') { // 18 = 17 + alternate identifiers; 19 = 18 + concrete values
|
|
1362
1362
|
result.hasLangs = true;
|
|
1363
1363
|
} else if (result.cacheVersion === '16') { // Assuming this is UTF8 version for now
|
|
1364
1364
|
result.isUTF16 = false;
|
|
@@ -1379,9 +1379,22 @@ class SnomedFileReader {
|
|
|
1379
1379
|
result.activeRoots.push(this.readUInt64());
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
|
-
// Read default language
|
|
1382
|
+
// Read default language
|
|
1383
1383
|
result.defaultLanguage = this.readInteger();
|
|
1384
1384
|
|
|
1385
|
+
// Alternate identifiers (version 18): [[scheme, module, code, conceptId], ...]
|
|
1386
|
+
result.identifiers = [];
|
|
1387
|
+
if (result.cacheVersion === '18' || result.cacheVersion === '19') {
|
|
1388
|
+
result.identifiers = JSON.parse(this.readBytes().toString('utf8'));
|
|
1389
|
+
}
|
|
1390
|
+
// Concrete value relationships (version 19): [[sourceId, typeId, group, value], ...].
|
|
1391
|
+
// null (not []) when the cache predates them, so the server can tell "none" from
|
|
1392
|
+
// "not loaded".
|
|
1393
|
+
result.concreteValues = null;
|
|
1394
|
+
if (result.cacheVersion === '19') {
|
|
1395
|
+
result.concreteValues = JSON.parse(this.readBytes().toString('utf8'));
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1385
1398
|
return result;
|
|
1386
1399
|
}
|
|
1387
1400
|
}
|
package/tx/tx.fhir.org.yml
CHANGED
|
@@ -20,7 +20,7 @@ sources:
|
|
|
20
20
|
- internal:hgvs
|
|
21
21
|
- ucum:tx/data/ucum-essence.xml
|
|
22
22
|
- loinc:loinc-2.77-a.db
|
|
23
|
-
- loinc!:Loinc_2.
|
|
23
|
+
- loinc!:Loinc_2.83.db
|
|
24
24
|
- rxnorm:rxnorm_03022026.db
|
|
25
25
|
- ndc:ndc-20211101.db
|
|
26
26
|
- unii:unii_20240622.db
|
|
@@ -32,7 +32,7 @@ sources:
|
|
|
32
32
|
- snomed:sct_us_20230301.cache
|
|
33
33
|
- snomed:sct_us_20240301.cache
|
|
34
34
|
- snomed:sct_us_20250901.cache
|
|
35
|
-
- snomed:
|
|
35
|
+
- snomed:sct_test_20250909e.cache
|
|
36
36
|
- cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
|
|
37
37
|
- omop:omop_v20250227.db
|
|
38
38
|
- npm:hl7.terminology=tho
|
package/tx/vs/vs-package.js
CHANGED
|
@@ -8,6 +8,7 @@ const { checkContained } = require('../library/canonical-resource');
|
|
|
8
8
|
const { VersionUtilities } = require('../../library/version-utilities');
|
|
9
9
|
const {validateParameter} = require("../../library/utilities");
|
|
10
10
|
const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
|
|
11
|
+
const { keepForPinnedReference } = require('../library/tho-precedence');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Package-based ValueSet provider using shared database layer
|
|
@@ -267,7 +268,7 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
267
268
|
|
|
268
269
|
// Only process each ValueSet once (use URL to deduplicate)
|
|
269
270
|
const vsUrl = json.url;
|
|
270
|
-
if (seen.has(vsUrl)) {
|
|
271
|
+
if (seen.has(vsUrl) || this.#excludedUrls.has(vsUrl)) {
|
|
271
272
|
continue;
|
|
272
273
|
}
|
|
273
274
|
|
|
@@ -460,35 +461,38 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
460
461
|
return this.#excludedUrls.size === 0 ? urls : urls.filter(url => !this.#excludedUrls.has(url));
|
|
461
462
|
}
|
|
462
463
|
|
|
463
|
-
// the value sets left out by
|
|
464
|
+
// the value sets left out by keepPinnedOnly - by url, and by their own (unprefixed) id
|
|
464
465
|
#excludedUrls = new Set();
|
|
465
466
|
#excludedIds = new Set();
|
|
466
467
|
|
|
467
468
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
469
|
+
* The value sets with these urls are hl7.terminology's to provide (see
|
|
470
|
+
* Provider.applyTHOPrecedence): from this (core) package they are only available to a
|
|
471
|
+
* reference that names their version, where THO doesn't provide that version itself. They
|
|
472
|
+
* aren't the default for their url, can't be read by id, aren't found by search or listed.
|
|
473
|
+
* Must be called once the provider is initialized - the database is shared, so it isn't
|
|
474
|
+
* changed.
|
|
472
475
|
*
|
|
473
|
-
* @param {Set<string
|
|
476
|
+
* @param {Map<string, Set<string>>} thoVersions - url -> the versions hl7.terminology provides
|
|
474
477
|
*/
|
|
475
|
-
|
|
476
|
-
const
|
|
478
|
+
keepPinnedOnly(thoVersions) {
|
|
479
|
+
const affected = new Set();
|
|
477
480
|
for (const vs of this.valueSetMap.values()) {
|
|
478
|
-
if (vs &&
|
|
479
|
-
|
|
481
|
+
if (vs && thoVersions.has(vs.url)) {
|
|
482
|
+
affected.add(vs);
|
|
480
483
|
}
|
|
481
484
|
}
|
|
482
485
|
for (const [key, vs] of [...this.valueSetMap]) {
|
|
483
|
-
if (
|
|
486
|
+
if (affected.has(vs) && !keepForPinnedReference(key, vs, thoVersions.get(vs.url))) {
|
|
484
487
|
this.valueSetMap.delete(key);
|
|
485
488
|
}
|
|
486
489
|
}
|
|
487
|
-
for (const vs of
|
|
490
|
+
for (const vs of affected) {
|
|
488
491
|
this.#excludedUrls.add(vs.url);
|
|
489
492
|
this.#excludedIds.add(stripIdPrefix(vs.id, this.spaceId) ?? vs.id);
|
|
490
493
|
}
|
|
491
494
|
}
|
|
495
|
+
|
|
492
496
|
}
|
|
493
497
|
|
|
494
498
|
module.exports = {
|
package/tx/vs/vs-vsac.js
CHANGED
|
@@ -42,7 +42,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
42
42
|
this.apiKey = config.apiKey;
|
|
43
43
|
// Optional operator password for the on-demand resync form (see info()).
|
|
44
44
|
this.resyncPassword = config.resyncPassword || null;
|
|
45
|
-
this.cacheFolder = folders.
|
|
45
|
+
this.cacheFolder = folders.ensureFolder("terminology-cache", "vsac");
|
|
46
46
|
// https: every request carries the API key. (VSAC redirects http to https, but only after
|
|
47
47
|
// the key has been sent in the clear)
|
|
48
48
|
this.baseUrl = (config.baseUrl || 'https://cts.nlm.nih.gov/fhir').replace(/\/+$/, '');
|