fhirsmith 0.13.3 → 0.14.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 +47 -1
- package/README.md +48 -4
- package/config-template.json +70 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/nginx.md +391 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher.js +146 -14
- package/server.js +51 -14
- package/stats.js +19 -13
- package/testing/readme.md +112 -0
- package/testing/render.js +482 -0
- package/testing/search.js +378 -0
- package/testing/store.js +266 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +486 -0
- package/translations/Messages.properties +32 -0
- package/tx/closure/closure-store.js +396 -0
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-rxnorm.js +35 -8
- package/tx/cs/cs-snomed.js +142 -27
- package/tx/html/codesystem-operations.liquid +177 -0
- package/tx/html/ecl-panel.liquid +93 -0
- package/tx/html/home-metrics.liquid +39 -76
- package/tx/html/tx-template.html +1 -0
- package/tx/library/conceptmap.js +22 -6
- package/tx/library/designations.js +12 -3
- package/tx/library/extensions.js +2 -2
- package/tx/library/operation-outcome.js +3 -1
- package/tx/library/renderer.js +42 -5
- package/tx/operation-context.js +111 -15
- package/tx/provider.js +85 -0
- package/tx/sct/expressions.js +128 -44
- package/tx/sct/text-index.js +419 -0
- package/tx/tx-html.js +426 -44
- package/tx/tx.js +117 -9
- package/tx/workers/cache-control.js +2 -2
- package/tx/workers/closure.js +483 -13
- package/tx/workers/expand.js +20 -0
- package/tx/workers/metadata.js +29 -4
- package/tx/workers/read.js +18 -3
- package/tx/workers/subsumes.js +18 -2
- package/tx/workers/translate.js +167 -20
- package/tx/workers/validate.js +61 -182
- package/tx/workers/worker.js +9 -0
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
package/tx/sct/expressions.js
CHANGED
|
@@ -26,6 +26,8 @@ const MRCM_ATTRIBUTE_RANGE_REFSET = 723562003n;
|
|
|
26
26
|
// rules about authoring precoordinated concepts and do not constrain expressions.
|
|
27
27
|
const MRCM_CONTENT_TYPE_ALL = 723596005n;
|
|
28
28
|
const MRCM_CONTENT_TYPE_POSTCOORDINATED = 723594008n;
|
|
29
|
+
// ruleStrengthId value for an optional rule (the other value, 723597001, is mandatory).
|
|
30
|
+
const MRCM_RULE_STRENGTH_OPTIONAL = 723598006n;
|
|
29
31
|
|
|
30
32
|
// Expression status enumeration
|
|
31
33
|
const SnomedExpressionStatus = {
|
|
@@ -44,6 +46,17 @@ class SnomedExpressionBase {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether an attribute value has to be written in brackets: the compositional grammar
|
|
51
|
+
* only allows a bare conceptReference as an attribute value, so anything with more than
|
|
52
|
+
* one focus concept, or with refinements of its own, is a bracketed subExpression.
|
|
53
|
+
*/
|
|
54
|
+
function needsBrackets(value) {
|
|
55
|
+
return !!value && ((value.concepts && value.concepts.length > 1) ||
|
|
56
|
+
(value.refinements && value.refinements.length > 0) ||
|
|
57
|
+
(value.refinementGroups && value.refinementGroups.length > 0));
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
/**
|
|
48
61
|
* Represents a SNOMED concept with optional code, description, literal, or decimal value
|
|
49
62
|
*/
|
|
@@ -161,7 +174,9 @@ class SnomedRefinement extends SnomedExpressionBase {
|
|
|
161
174
|
* Get a string description of this refinement
|
|
162
175
|
*/
|
|
163
176
|
describe() {
|
|
164
|
-
|
|
177
|
+
// A refined or conjoined value has to be bracketed - see attributeValue() in the parser
|
|
178
|
+
const v = this.value.describe();
|
|
179
|
+
return this.name.describe() + '=' + (needsBrackets(this.value) ? '(' + v + ')' : v);
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
/**
|
|
@@ -647,16 +662,33 @@ class SnomedExpressionParser {
|
|
|
647
662
|
|
|
648
663
|
/**
|
|
649
664
|
* Parse an attribute value
|
|
665
|
+
*
|
|
666
|
+
* The compositional grammar ABNF is
|
|
667
|
+
* attributeValue = expressionValue / QM stringValue QM / "#" numericValue / booleanValue
|
|
668
|
+
* expressionValue = ws ( conceptReference / "(" ws subExpression ws ")" ) ws
|
|
669
|
+
* so a value that is anything more than a single concept - a refined concept, or a
|
|
670
|
+
* conjunction - must be in brackets. This parser used to take an unbracketed
|
|
671
|
+
* sub-expression too (367430006:{405813007=85562004:272741003=24028007}), but that is not
|
|
672
|
+
* just non-conformant, it is ambiguous: in A:{B=C:D=E,F=G} nothing says whether F=G
|
|
673
|
+
* refines C or A, and the old parser silently picked C. Other parsers reject it, so we
|
|
674
|
+
* do too, and the renderer brackets nested values so that what we emit re-parses anywhere.
|
|
650
675
|
*/
|
|
651
676
|
attributeValue() {
|
|
652
677
|
this.ws();
|
|
653
678
|
|
|
654
679
|
if (this.gchar('(')) {
|
|
655
680
|
const result = this.expression();
|
|
681
|
+
this.ws();
|
|
656
682
|
this.fixed(')');
|
|
657
683
|
return result;
|
|
658
684
|
} else {
|
|
659
|
-
|
|
685
|
+
const result = new SnomedExpression();
|
|
686
|
+
result.start = this.cursor;
|
|
687
|
+
result.concepts.push(this.concept());
|
|
688
|
+
this.rule(this.peek() !== ':' && this.peek() !== '+',
|
|
689
|
+
'A refined or conjoined attribute value must be enclosed in brackets, e.g. 405813007=(85562004:272741003=24028007) (in "' + this.source + '")');
|
|
690
|
+
result.stop = this.cursor;
|
|
691
|
+
return result;
|
|
660
692
|
}
|
|
661
693
|
}
|
|
662
694
|
|
|
@@ -1625,7 +1657,7 @@ class SnomedExpressionServices {
|
|
|
1625
1657
|
this.checkRefinement(refinement);
|
|
1626
1658
|
this.checkRefinementDomain(expression, refinement);
|
|
1627
1659
|
this.checkRefinementRange(refinement);
|
|
1628
|
-
this.checkRefinementGrouped(refinement);
|
|
1660
|
+
this.checkRefinementGrouped(expression, refinement);
|
|
1629
1661
|
}
|
|
1630
1662
|
}
|
|
1631
1663
|
|
|
@@ -1644,12 +1676,8 @@ class SnomedExpressionServices {
|
|
|
1644
1676
|
|
|
1645
1677
|
/**
|
|
1646
1678
|
* Check one refinement against the MRCM attribute domain reference set: the
|
|
1647
|
-
* focus concept the attribute is applied to must be in the attribute's
|
|
1648
|
-
*
|
|
1649
|
-
* Every applicable rule must hold, mandatory or optional. Internationally
|
|
1650
|
-
* there is exactly one optional rule - Laterality in the lateralizable body
|
|
1651
|
-
* structure reference set - and it is the rule that stops laterality being
|
|
1652
|
-
* applied to a structure that cannot be lateralized, so it is enforced.
|
|
1679
|
+
* focus concept the attribute is applied to must be in one of the attribute's
|
|
1680
|
+
* domains (see applicableDomainRules for how several rules combine).
|
|
1653
1681
|
*
|
|
1654
1682
|
* Attributes with no rule in this edition's MRCM are not checked, and neither
|
|
1655
1683
|
* is cardinality (checkExpressionCardinality), grouping (checkRefinementGrouped)
|
|
@@ -1663,44 +1691,84 @@ class SnomedExpressionServices {
|
|
|
1663
1691
|
if (attribute === undefined || attribute === null || attribute === NO_REFERENCE) {
|
|
1664
1692
|
return;
|
|
1665
1693
|
}
|
|
1666
|
-
const
|
|
1667
|
-
if (
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1694
|
+
const result = this.applicableDomainRules(expression, attribute);
|
|
1695
|
+
if (result.failed) {
|
|
1696
|
+
const attrDesc = this.describeConceptForMessage(attribute);
|
|
1697
|
+
const focusDesc = result.focus.map(f => this.describeConceptForMessage(f)).join(' + ');
|
|
1698
|
+
throw new Error(`The SNOMED CT concept model does not allow the attribute ${attrDesc} on ${focusDesc}`
|
|
1699
|
+
+ ` (the attribute is only valid ${result.failed})`);
|
|
1672
1700
|
}
|
|
1701
|
+
}
|
|
1673
1702
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1703
|
+
/**
|
|
1704
|
+
* Work out which of an attribute's MRCM domain rules apply to an expression's
|
|
1705
|
+
* focus concept(s), and whether the attribute is allowed there at all.
|
|
1706
|
+
*
|
|
1707
|
+
* An attribute has one domain rule per domain it may be used in, so the
|
|
1708
|
+
* mandatory rules are ALTERNATIVES: |Due to| has one rule for |Clinical finding|
|
|
1709
|
+
* and one for |Event|, and is valid on a concept in either (issue #287 - these
|
|
1710
|
+
* were once ANDed, which rejected |Due to| everywhere).
|
|
1711
|
+
*
|
|
1712
|
+
* Optional rules are RESTRICTIONS layered on top: internationally the only one
|
|
1713
|
+
* is |Laterality| for the lateralizable body structure reference set, which
|
|
1714
|
+
* narrows the mandatory |Anatomical structure| rule, and it is what stops
|
|
1715
|
+
* laterality being applied to a structure that cannot be lateralized. So an
|
|
1716
|
+
* optional rule must also hold wherever a mandatory rule has admitted the
|
|
1717
|
+
* attribute. (An attribute with only optional rules treats them as
|
|
1718
|
+
* alternatives, since there is nothing for them to narrow.)
|
|
1719
|
+
*
|
|
1720
|
+
* The focus may be a conjunction of concepts; the expression is a subtype of
|
|
1721
|
+
* each of them, so a rule holds if it holds for any focus concept.
|
|
1722
|
+
*
|
|
1723
|
+
* @param {SnomedExpression} expression
|
|
1724
|
+
* @param {number} attribute - concept index of the attribute
|
|
1725
|
+
* @returns {{rules: Array, focus: number[], failed: string|null}} the rules that
|
|
1726
|
+
* apply (for cardinality and grouping - empty when the attribute is out of
|
|
1727
|
+
* domain), the focus concepts, and when the attribute is not allowed on this
|
|
1728
|
+
* focus, a description of the domain(s) it is restricted to
|
|
1729
|
+
*/
|
|
1730
|
+
applicableDomainRules(expression, attribute) {
|
|
1731
|
+
const rules = this.mrcmAttributeDomains().get(attribute);
|
|
1676
1732
|
const focus = [];
|
|
1677
1733
|
for (const concept of expression.concepts) {
|
|
1678
1734
|
if (concept.reference !== undefined && concept.reference !== null && concept.reference !== NO_REFERENCE) {
|
|
1679
1735
|
focus.push(concept.reference);
|
|
1680
1736
|
}
|
|
1681
1737
|
}
|
|
1682
|
-
if (focus.length === 0) {
|
|
1683
|
-
|
|
1738
|
+
if (!rules || rules.length === 0 || focus.length === 0) {
|
|
1739
|
+
// This edition's concept model says nothing about this attribute (3 active
|
|
1740
|
+
// attributes internationally have no domain rule and appear in no
|
|
1741
|
+
// postcoordination domain template), so there is nothing to check it against.
|
|
1742
|
+
return { rules: rules || [], focus, failed: null };
|
|
1684
1743
|
}
|
|
1685
1744
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1745
|
+
const holds = rule => focus.some(f => rule.isRefset
|
|
1746
|
+
? this.conceptInRefSet(rule.domain, f)
|
|
1747
|
+
: this.subsumes(rule.domain, f));
|
|
1748
|
+
const describe = rule => (rule.isRefset ? 'for concepts in ' : 'in the domain ')
|
|
1749
|
+
+ this.describeConceptForMessage(rule.domain);
|
|
1750
|
+
|
|
1751
|
+
const mandatory = rules.filter(rule => !rule.optional);
|
|
1752
|
+
const alternatives = mandatory.length > 0 ? mandatory : rules;
|
|
1753
|
+
const restrictions = mandatory.length > 0 ? rules.filter(rule => rule.optional) : [];
|
|
1754
|
+
|
|
1755
|
+
const matched = alternatives.filter(holds);
|
|
1756
|
+
const failedRestrictions = restrictions.filter(rule => !holds(rule));
|
|
1757
|
+
if (matched.length === 0) {
|
|
1758
|
+
// Name every domain the attribute could have been used in, and any
|
|
1759
|
+
// restriction that also fails: a laterality on a disorder is outside both
|
|
1760
|
+
// the anatomical structure domain and the lateralizable refset, and naming
|
|
1761
|
+
// only one of them misleads.
|
|
1762
|
+
const parts = [alternatives.map(describe).join(', or ')];
|
|
1763
|
+
for (const rule of failedRestrictions) {
|
|
1764
|
+
parts.push(describe(rule));
|
|
1696
1765
|
}
|
|
1766
|
+
return { rules: [], focus, failed: parts.join(', and ') };
|
|
1697
1767
|
}
|
|
1698
|
-
if (
|
|
1699
|
-
|
|
1700
|
-
const focusDesc = focus.map(f => this.describeConceptForMessage(f)).join(' + ');
|
|
1701
|
-
throw new Error(`The SNOMED CT concept model does not allow the attribute ${attrDesc} on ${focusDesc}`
|
|
1702
|
-
+ ` (the attribute is only valid ${failed.join(', and ')})`);
|
|
1768
|
+
if (failedRestrictions.length > 0) {
|
|
1769
|
+
return { rules: [], focus, failed: failedRestrictions.map(describe).join(', and ') };
|
|
1703
1770
|
}
|
|
1771
|
+
return { rules: matched.concat(restrictions), focus, failed: null };
|
|
1704
1772
|
}
|
|
1705
1773
|
|
|
1706
1774
|
/**
|
|
@@ -1721,17 +1789,19 @@ class SnomedExpressionServices {
|
|
|
1721
1789
|
* expression is not classifiable is honest, quietly guessing what they meant is not.
|
|
1722
1790
|
*
|
|
1723
1791
|
* As elsewhere in this check, an attribute with no rule in this edition's MRCM is
|
|
1724
|
-
* left alone
|
|
1792
|
+
* left alone. Only the rules for the domain(s) the focus concept is in count
|
|
1793
|
+
* (see applicableDomainRules), and where several of those apply the strictest wins.
|
|
1725
1794
|
*
|
|
1795
|
+
* @param {SnomedExpression} expression - the expression the refinement is on
|
|
1726
1796
|
* @param {SnomedRefinement} refinement - a refinement outside any group
|
|
1727
1797
|
*/
|
|
1728
|
-
checkRefinementGrouped(refinement) {
|
|
1798
|
+
checkRefinementGrouped(expression, refinement) {
|
|
1729
1799
|
const attribute = refinement.name ? refinement.name.reference : NO_REFERENCE;
|
|
1730
1800
|
if (attribute === undefined || attribute === null || attribute === NO_REFERENCE) {
|
|
1731
1801
|
return;
|
|
1732
1802
|
}
|
|
1733
|
-
const rules = this.
|
|
1734
|
-
if (
|
|
1803
|
+
const rules = this.applicableDomainRules(expression, attribute).rules;
|
|
1804
|
+
if (rules.length === 0) {
|
|
1735
1805
|
return;
|
|
1736
1806
|
}
|
|
1737
1807
|
if (!rules.some(rule => rule.grouped)) {
|
|
@@ -1747,7 +1817,7 @@ class SnomedExpressionServices {
|
|
|
1747
1817
|
* Built once per edition and cached; an edition with no MRCM yields an empty
|
|
1748
1818
|
* map, which turns the domain check off rather than failing.
|
|
1749
1819
|
*
|
|
1750
|
-
* @returns {Map<number, Array<{domain: number, isRefset: boolean, grouped: boolean, max: number, maxInGroup: number}>>}
|
|
1820
|
+
* @returns {Map<number, Array<{domain: number, isRefset: boolean, grouped: boolean, optional: boolean, max: number, maxInGroup: number}>>}
|
|
1751
1821
|
*/
|
|
1752
1822
|
mrcmAttributeDomains() {
|
|
1753
1823
|
if (this._mrcmAttributeDomains) {
|
|
@@ -1762,6 +1832,7 @@ class SnomedExpressionServices {
|
|
|
1762
1832
|
}
|
|
1763
1833
|
const contentAll = this.conceptIndexOf(MRCM_CONTENT_TYPE_ALL);
|
|
1764
1834
|
const contentPost = this.conceptIndexOf(MRCM_CONTENT_TYPE_POSTCOORDINATED);
|
|
1835
|
+
const strengthOptional = this.conceptIndexOf(MRCM_RULE_STRENGTH_OPTIONAL);
|
|
1765
1836
|
|
|
1766
1837
|
// Every concept that defines a reference set, so a domain that is itself a
|
|
1767
1838
|
// reference set (the lateralizable body structure refset) is tested by
|
|
@@ -1804,6 +1875,9 @@ class SnomedExpressionServices {
|
|
|
1804
1875
|
// The grouped column is a plain 0/1 (type 4), not a reference into the string
|
|
1805
1876
|
// table like the cardinalities beside it - reading it as one yields garbage.
|
|
1806
1877
|
grouped: values[2] === 1,
|
|
1878
|
+
// An optional rule narrows a mandatory one rather than adding a domain
|
|
1879
|
+
// (see applicableDomainRules).
|
|
1880
|
+
optional: values[8] === strengthOptional,
|
|
1807
1881
|
// Only the maximums are enforced. A minimum ("1..1") says a concept in
|
|
1808
1882
|
// this domain must have the attribute in its own definition; it does not
|
|
1809
1883
|
// say a refinement of that concept has to restate it.
|
|
@@ -2033,8 +2107,10 @@ class SnomedExpressionServices {
|
|
|
2033
2107
|
* a single relationship group - a maximum of 0 there is how the MRCM says an
|
|
2034
2108
|
* attribute is ungrouped (all 37 ungrouped attributes carry "0..0").
|
|
2035
2109
|
*
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2110
|
+
* Only the rules for the domain(s) the focus concept is in count - |Procedure
|
|
2111
|
+
* device| is 0..* in a group on a procedure but 0..1 on an observable, and a
|
|
2112
|
+
* procedure must not get the observable's limit (see applicableDomainRules).
|
|
2113
|
+
* Where several of those rules apply, the tightest maximum wins. Nested value expressions are checked separately, when
|
|
2038
2114
|
* checkRefinement recurses into them.
|
|
2039
2115
|
*
|
|
2040
2116
|
* @param {SnomedExpression} expression
|
|
@@ -2072,10 +2148,14 @@ class SnomedExpressionServices {
|
|
|
2072
2148
|
perGroup.push(counts);
|
|
2073
2149
|
}
|
|
2074
2150
|
|
|
2151
|
+
const applicable = new Map();
|
|
2075
2152
|
const limit = (attribute, field) => {
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2153
|
+
if (!applicable.has(attribute)) {
|
|
2154
|
+
applicable.set(attribute, this.applicableDomainRules(expression, attribute).rules);
|
|
2155
|
+
}
|
|
2156
|
+
const rules = applicable.get(attribute);
|
|
2157
|
+
if (rules.length === 0) {
|
|
2158
|
+
return Infinity; // no rule, or out of domain (which checkRefinementDomain reports)
|
|
2079
2159
|
}
|
|
2080
2160
|
return rules.reduce((least, rule) => Math.min(least, rule[field]), Infinity);
|
|
2081
2161
|
};
|
|
@@ -2529,7 +2609,11 @@ class SnomedExpressionServices {
|
|
|
2529
2609
|
renderRefinement(parts, expr, option) {
|
|
2530
2610
|
this.renderConcept(parts, expr.name, option);
|
|
2531
2611
|
parts.push('=');
|
|
2612
|
+
// a refined or conjoined value must be bracketed, or no conformant parser will read it back
|
|
2613
|
+
const bracket = needsBrackets(expr.value);
|
|
2614
|
+
if (bracket) parts.push('(');
|
|
2532
2615
|
this.renderExpressionParts(parts, expr.value, option);
|
|
2616
|
+
if (bracket) parts.push(')');
|
|
2533
2617
|
}
|
|
2534
2618
|
|
|
2535
2619
|
getDisplayName(conceptIdOrReference) {
|