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
@@ -209,6 +209,7 @@ const OPERATIONS = [
209
209
  p('version', 'string', '1', 'The version of the code system'),
210
210
  p('coding', 'Coding', '1', 'The code, as a Coding'),
211
211
  p('property', 'code', '*', 'A property to return (by default, all of them)'),
212
+ p('post-coordination-metadata', 'boolean', '1', 'For a post-coordinated code: describe every post-coordination axis in full - the axis rules, and the axes that carry no value (ICD-11). By default only the axes that carry a value are listed'),
212
213
  ],
213
214
  out: [
214
215
  p('name', 'string', '1', 'The name of the code system', { min: 1 }),
@@ -283,11 +284,14 @@ const OPERATIONS = [
283
284
  parts: [
284
285
  p('relationship', 'code', '1', 'How the target relates to the source'),
285
286
  p('concept', 'Coding', '1', 'The target'),
287
+ p('normalized-concept', 'Coding', '1', 'The target in its code system\'s normal form, when the map states it in another form'),
288
+ p('note', 'string', '1', 'The target\'s code system is not known, so the code has not been validated'),
286
289
  p('product', '(parts)', '*', 'Another element of the product (element, concept)'),
287
290
  p('originMap', 'canonical', '1', 'The concept map'),
288
291
  p('sourceConcept', 'Coding', '1', 'What was translated (for a translation in reverse)')
289
292
  ]
290
293
  }),
294
+ p('error', 'string', '*', 'A target code the maps gave that is not valid in its code system'),
291
295
  p('used-conceptmap', 'canonical', '*', 'A concept map that was used')
292
296
  ]
293
297
  },
package/tx/params.js CHANGED
@@ -380,6 +380,12 @@ class TxParameters {
380
380
  if (claim('includeDefinition')) this.includeDefinition = asBool();
381
381
  break;
382
382
  }
383
+ // $lookup: describe each postcoordination axis in full (ICD-11) - the axis rules, and the
384
+ // axes that carry no value. Off unless asked for
385
+ case 'post-coordination-metadata': {
386
+ if (claim('post-coordination-metadata')) this.postCoordinationMetadata = asBool();
387
+ break;
388
+ }
383
389
  case 'activeOnly': {
384
390
  if (claim('activeOnly')) this.activeOnly = asBool();
385
391
  break;
@@ -624,7 +630,7 @@ class TxParameters {
624
630
  // are included, and diagnostics.
625
631
  // filter is free text, so JSON.stringify it to avoid delimiter collisions.
626
632
  s = s + 'f:' + JSON.stringify(this.filter || '') + '|' +
627
- b(this.abstractOk) + b(this.diagnostics);
633
+ b(this.abstractOk) + b(this.diagnostics) + b(this.postCoordinationMetadata);
628
634
  for (let t of this.FVersionRules) {
629
635
  s = s + t.asString() + '|';
630
636
  }
@@ -676,6 +682,7 @@ class TxParameters {
676
682
  this.hasVersionsMatch = other.hasVersionsMatch;
677
683
  this.hasDisplayWarning = other.hasDisplayWarning;
678
684
  this.sort = other.sort;
685
+ this.postCoordinationMetadata = other.postCoordinationMetadata;
679
686
 
680
687
  if (other.FSeen) {
681
688
  this.FSeen = new Set(other.FSeen);
package/tx/problems.js CHANGED
@@ -36,7 +36,7 @@ class ProblemFinder {
36
36
  }
37
37
  let list = await this.unknownVersionsHtml(sourceUnknownVersions, provider, sp);
38
38
  if (list) {
39
- result = result + `<h4>${sp}</h4>` + list;
39
+ result = result + `<h4>${escape(sp)}</h4>` + list;
40
40
  }
41
41
  }
42
42
  }
@@ -51,14 +51,16 @@ class ProblemFinder {
51
51
  entries.sort((a, b) => a[0].localeCompare(b[0]));
52
52
  let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th><th>Known Versions</th></tr></thead><tbody>';
53
53
  for (const [system, vset] of entries) {
54
- const systemEsc = escape(system);
54
+ // values from the loaded packages: URL-encoded in links, HTML-escaped everywhere
55
+ const elements = '&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate';
55
56
  const versions = [...vset].sort((a, b) => a.localeCompare(b));
56
57
  const versionRefs = [];
57
58
  for (const v of versions) {
58
- versionRefs.push(`<a href="ValueSet?system=${systemEsc}|${escape(v)}&source=${source}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${v}</a>`);
59
+ const href = `ValueSet?system=${encodeURIComponent(system + '|' + v)}&source=${encodeURIComponent(source)}${elements}`;
60
+ versionRefs.push(`<a href="${escape(href)}">${escape(v)}</a>`);
59
61
  }
60
- const knownVersions = [...await provider.listCodeSystemVersions(system)].join('<br/>');
61
- html += `<tr><td><a href="CodeSystem?url=${systemEsc}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${system}</a></td>`+
62
+ const knownVersions = [...await provider.listCodeSystemVersions(system)].map(v => escape(v)).join('<br/>');
63
+ html += `<tr><td><a href="${escape(`CodeSystem?url=${encodeURIComponent(system)}${elements}`)}">${escape(system)}</a></td>`+
62
64
  `<td>${versionRefs.join('<br/>')}</td><td>${knownVersions}</td></tr>`;
63
65
  }
64
66
  html += '</tbody></table>';
package/tx/provider.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { CodeSystem } = require("./library/codesystem");
2
+ const { assertNoActiveContent } = require('../library/narrative-check');
2
3
  const {VersionUtilities} = require("../library/version-utilities");
3
4
  const { FhirCodeSystemProvider} = require("./cs/cs-cs");
4
5
  const {OperationContext} = require("./operation-context");
@@ -150,7 +151,7 @@ class Provider {
150
151
  validateParameter(opContext, "opContext", OperationContext);
151
152
  validateParameter(codeSystem, "codeSystem", CodeSystem);
152
153
  validateArrayParameter(supplements, "supplements", CodeSystem);
153
- return new FhirCodeSystemProvider(opContext, codeSystem, supplements);
154
+ return FhirCodeSystemProvider.create(opContext, codeSystem, supplements);
154
155
  }
155
156
 
156
157
 
@@ -159,6 +160,8 @@ class Provider {
159
160
  * space (CodeSystem/core-v3-MaritalStatus): the core packages for different FHIR versions
160
161
  * share ids, and so does other content (hl7.terminology), so no endpoint can serve them on
161
162
  * their own ids (see tx/library/resource-ids.js)
163
+ *
164
+ * @returns {Promise<PackageContentLoader|undefined>} the loaded package (mode "npm")
162
165
  */
163
166
  async loadNpm(packageManager, cacheFolder, details, isDefault, mode) {
164
167
  // Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2")
@@ -184,7 +187,10 @@ class Provider {
184
187
  for (const resource of resources) {
185
188
  let cs;
186
189
  try {
187
- cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
190
+ const json = await contentLoader.loadFile(resource, contentLoader.fhirVersion());
191
+ // narrative is shown as-is in a page, so it must not carry script (see narrative-check.js)
192
+ assertNoActiveContent(json);
193
+ cs = new CodeSystem(json);
188
194
  } catch (e) {
189
195
  // one unacceptable resource (e.g. contained resources the server doesn't support)
190
196
  // must not stop the rest of the package loading
@@ -212,6 +218,7 @@ class Provider {
212
218
  await cm.initialize();
213
219
  cm.assignIds(CORE_PREFIX);
214
220
  this.conceptMapProviders.push(cm);
221
+ return contentLoader;
215
222
  }
216
223
 
217
224
  getCodeSystemById(opContext, id) {
@@ -327,16 +334,25 @@ class Provider {
327
334
 
328
335
 
329
336
 
330
- async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode = null) {
337
+ async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceVersion, sourceScope, targetScope, targetSystem, sourceCode = null) {
331
338
  for (let cmp of this.conceptMapProviders) {
332
339
  await cmp.findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode);
333
340
  }
334
341
  if (sourceSystem && targetSystem) {
342
+ // Each code system is asked once - except the source system itself, where every
343
+ // version (every SNOMED edition, say) is asked, since whether a map exists and what
344
+ // it says can depend on which version the source coding is in. The factory decides
345
+ // from sourceVersion whether it is the one being asked about.
335
346
  let uris = new Set();
347
+ let asked = new Set(); // a factory is registered under several keys (system, system|version...)
336
348
  for (let csp of this.codeSystemFactories.values()) {
337
- if (!uris.has(csp.system())) {
349
+ if (asked.has(csp)) {
350
+ continue;
351
+ }
352
+ if (csp.system() === sourceSystem || !uris.has(csp.system())) {
338
353
  uris.add(csp.system());
339
- await csp.findImplicitConceptMaps(conceptMaps, sourceSystem, targetSystem);
354
+ asked.add(csp);
355
+ await csp.findImplicitConceptMaps(conceptMaps, sourceSystem, sourceVersion, targetSystem);
340
356
  }
341
357
  }
342
358
  }
@@ -0,0 +1,251 @@
1
+ # SNOMED CT support in FHIRsmith: known limitations
2
+
3
+ This file records what the SNOMED CT code system provider (`tx/cs/cs-snomed.js`,
4
+ `tx/sct/*`) does not do, so that nobody has to rediscover it from the code. It is
5
+ organised by area: ECL, post-coordinated expressions, and everything else. Where
6
+ a limitation is a deliberate decision rather than unfinished work, it says so.
7
+
8
+ The import pipeline (`tx/importers/import-sct.module.js`) reads the RF2 snapshot
9
+ concept, description (including text definitions, which share the description
10
+ file layout), inferred relationship, and reference set files. It does not read
11
+ stated relationships, the OWL axiom reference sets, or the concrete value
12
+ relationship file; several of the limitations below follow from that.
13
+
14
+ ## ECL
15
+
16
+ The parser (`tx/sct/ecl.js`) accepts the ECL 2.2 grammar with the exceptions
17
+ listed under "Not parsed". The evaluator (`SnomedServices.filterECL` and the
18
+ `_eval*` methods in `cs-snomed.js`) evaluates the subset listed under
19
+ "Evaluated"; anything the parser accepts but the evaluator does not implement
20
+ is reported as `UNSUPPORTED_ECL`, and a syntax error or an unknown concept as
21
+ `INVALID_ECL`. Both are OperationOutcomes with tx-issue-type `vs-invalid`.
22
+
23
+ Note that `ECLValidator.evaluate*` in `ecl.js` is an older, weaker evaluator
24
+ that the server does not use. `filterECL` is the one that matters.
25
+
26
+ ### Evaluated
27
+
28
+ - All eight hierarchy operators (`<`, `<<`, `<!`, `<<!`, `>`, `>>`, `>!`, `>>!`)
29
+ on a concept reference, on `*`, on `^ refset` and on a bracketed expression
30
+ (applied to every member of the set), and top/bottom of set (`!!> (...)`,
31
+ `!!< (...)`).
32
+ - `*` (every concept, active and inactive - `activeOnly` is the expansion's job).
33
+ - `^` member-of on a concept or any bracketed expression, with
34
+ `{{ M ... }}` member filters on `effectiveTime`, `active`, `moduleId` and the
35
+ reference set's own fields (concept, integer and string typed from the RF2
36
+ file name). Since inactive member rows are dropped at import, `active = false`
37
+ never matches.
38
+ - `AND` / `OR` / `MINUS` (and `,`), with the ECL 6.4 rule that conjunction and
39
+ 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
+ - Dotted expressions (`< X . 363698007`, `. << 363698007`, `. (A OR B)`).
47
+ - `{{ C ... }}` concept filters on `active`, `definitionStatus`,
48
+ `definitionStatusId`, `moduleId` and `effectiveTime`.
49
+ - `{{ D ... }}` description filters on `term` (match semantics: each word of
50
+ the search term starts a word of the description, case-insensitively; and
51
+ `wild:` whole-term patterns), `language`, `typeId` / `type` (syn, fsn, def),
52
+ `dialectId` / `dialect` with acceptability sets (`(prefer)`, `(accept)` or
53
+ acceptability concept ids; the `dialect` aliases known are en-us, en-gb,
54
+ en-au, en-ca, en-nz, en-ie, fr-ca, fr-be, nl-be, nl-nl, es-es, sv-se, da-dk),
55
+ `moduleId`, `effectiveTime`, `active` and `id`. The filters in one block
56
+ must be satisfied by a single description; separate blocks may be satisfied
57
+ by different descriptions. Only active descriptions are considered unless
58
+ the block has an `active` filter.
59
+ - Comments (`/* ... */`), terms (`|...|`, not checked against descriptions).
60
+
61
+ ### Parsed but reported as unsupported
62
+
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
+ - Reference set field selection on member-of (`^ [targetComponentId] refset`,
73
+ `^ [referencedComponentId, targetComponentId] refset`). Deliberate: the
74
+ selected fields, and the referenced components of many reference sets,
75
+ are descriptions or other non-concept components, which an expansion has
76
+ 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
+
80
+ ### Not parsed
81
+
82
+ - `wild:` search terms are rejected in member filters (accepted in refinement
83
+ string values, which are then unsupported anyway).
84
+ - A concept id must be 6-18 digits; anything else is a syntax error (this is
85
+ what makes `<< abc` invalid rather than unsupported).
86
+
87
+ ### Other ECL notes
88
+
89
+ - Description filter term matching walks the descriptions of every concept in
90
+ the result set rather than using the text index, so `* {{ D term = "x" }}`
91
+ on a full edition scans every description. It yields to the event loop via
92
+ the operation's dead-check but is not fast.
93
+ - A `dialect` alias the server does not know is reported as unsupported, not
94
+ invalid; the alias table is `ECL_DIALECT_ALIASES` in `cs-snomed.js`.
95
+
96
+ - Term checking: a wrong term after a concept id (`10200004 |Lever structure|`)
97
+ is accepted silently. The tx-ecosystem test `expand-ecl-term-mismatch` expects
98
+ this.
99
+ - `^` on a concept that is not a reference set yields an empty result rather
100
+ than an error (Snowstorm's behaviour; agreed in the tx-ecosystem suite).
101
+ `^` on a reference set whose members are descriptions (a language reference
102
+ set) is reported as unsupported - there are no concepts to return.
103
+ - The ECL result for a value set filter is cached per (value set, code system
104
+ version), so the first expansion of a large constraint is slow and later ones
105
+ are not.
106
+
107
+ ## Post-coordinated expressions
108
+
109
+ Expressions are parsed by `SnomedExpressionParser` (`tx/sct/expressions.js`)
110
+ in SNOMED Compositional Grammar, including `===` / `<<<` definition status
111
+ markers, nested (bracketed) attribute values, concrete values and terms.
112
+ They are normalised, checked against the MRCM, and compared structurally.
113
+
114
+ ### Validation
115
+
116
+ `checkExpression` rejects an expression whose refinements break the concept
117
+ model (`$validate-code` then answers `result = false`): the attribute must be
118
+ allowed on the focus concept's domain (MRCM attribute domain reference set
119
+ `723561005`), the value must satisfy the attribute's range (`723562003`,
120
+ whether that is ECL or a concrete range such as `dec(>#0..)`), the attribute
121
+ and in-group cardinality maximums apply, and an attribute the MRCM marks as
122
+ grouped must be in a role group. Known limits:
123
+
124
+ - Minimum cardinality is not enforced (it constrains a concept's own
125
+ definition, not a refinement of it). The converse of the grouping rule
126
+ (grouped = 0 means must not be grouped) is enforced only through the
127
+ in-group cardinality `0..0`.
128
+ - Attributes with no MRCM rule in the loaded edition are not checked at all.
129
+ Internationally that is `704347000 |Observes|`, `131195008 |Subject of
130
+ information|` and `363705008 |Has definitional manifestation|`, and
131
+ `762951001 |Has ingredient|`, whose only domain row is scoped to new
132
+ precoordinated content (an MRCM inconsistency, not something to work around).
133
+ - An edition with no MRCM reference sets (e.g. the committed jest test cache)
134
+ gets no concept model checking at all; the checks go inert.
135
+ - The ungrouped "Close To User Form" is rejected rather than transformed to the
136
+ classifiable form. None of the seven Level 1 transformations in SNOMED's
137
+ postcoordination guide (refining an existing attribute, adding a self-grouped
138
+ attribute, adding severity, lateralising findings and procedures, adding
139
+ context to findings and procedures) is implemented.
140
+ - Negative concrete values (`#-5`) fail to parse ("Expected character } but
141
+ found -"). SCG permits them.
142
+ - Laterality on a non-lateralisable body structure is caught only when the
143
+ edition carries reference set `723264001`; the tx-ecosystem test edition
144
+ does not, so the IG suite cannot assert it.
145
+
146
+ ### Subsumption
147
+
148
+ `$subsumes` on expressions works by comparing normal forms: each side is
149
+ expanded through its fully defined concepts (primitives keep their atom and
150
+ contribute their defining attributes), then matched structurally, trying every
151
+ role group pairing.
152
+
153
+ - Positive answers (`subsumes`, `subsumed-by`, `equivalent`) are sound.
154
+ A negative answer is not provable this way: SNOMED's OWL axiom reference set
155
+ carries general concept inclusions and property chains whose entailments are
156
+ baked into the distributed inferred relationships for precoordinated concepts,
157
+ but an expression is something the classifier never saw. So when no
158
+ relationship is found and either side is an expression, the server returns an
159
+ OperationOutcome (HTTP 422, issue code `not-supported`, tx-issue-type
160
+ `cannot-determine`) rather than `not-subsumed`. Between two plain concepts all
161
+ four outcomes are sound (the transitive closure is the classifier's own
162
+ output).
163
+ - There is no reasoner. Removing the decline above would need an OWL
164
+ classifier over the axiom reference set (which is not imported).
165
+ - Normalisation depth is capped at 50 and throws rather than truncating,
166
+ because a truncated normal form is unsound in both directions.
167
+ - `subsumesTest` does not re-validate its inputs; `$subsumes` runs `locate()`
168
+ (and so `checkExpression`) on both codes first.
169
+
170
+ ### Lookup and display
171
+
172
+ - `$lookup` on an expression returns the rendered expression and its
173
+ components; the normal form, parents and children properties are returned
174
+ for single-concept expressions only.
175
+ - The display of an expression is the server's own rendering (no spaces,
176
+ `:{...}`), and every concept in it is given its preferred term.
177
+
178
+ ## Everything else
179
+
180
+ ### Data not imported
181
+
182
+ - Stated relationships and the OWL axiom reference sets. Everything is driven
183
+ by the distributed inferred relationships; there is no classification.
184
+ - Concrete value relationships (see ECL above).
185
+ - Alternate identifier reference sets.
186
+ - Inactive reference set member rows (only active rows are stored).
187
+ - Module ids and definition status on concepts are recorded by the current
188
+ importer; caches built before mid-2026 have neither (every concept looks
189
+ fully defined, module is 0). The jest cache `tx/data/snomed-testing.cache`
190
+ is one of those.
191
+
192
+ ### Language and display
193
+
194
+ - Description languages are mapped to a small fixed set at import
195
+ (`mapLanguageCode`); an unknown language is recorded as English. Norwegian
196
+ or Finnish descriptions, say, would be labelled `en`.
197
+ - `SnomedExpressionServices.codeForLanguage` has a stale language map that
198
+ disagrees with the importer; only the term is read from it today.
199
+ - With no `displayLanguage`, every edition defaults to US then GB English
200
+ preferred synonyms. A concept whose only English description is its FSN
201
+ gets its national-language preferred synonym instead.
202
+ - `sharedData.defaultLanguage` is hard-coded to 1 at import and
203
+ `getDisplayName` ignores the language argument it is passed.
204
+
205
+ ### Filters and implicit resources
206
+
207
+ - ValueSet filters supported on `compose.include.filter`: `concept` (`is-a`,
208
+ `descendent-of`, `in`), `inactive =`, `moduleId =`, `constraint =` (ECL),
209
+ `expressions =`, and `<attributeId> = <conceptId>`. Other operators on these
210
+ properties, and other properties, are rejected.
211
+ - Implicit value sets: `?fhir_vs`, `?fhir_vs=refset`, `?fhir_vs=refset/<id>`
212
+ and `?fhir_vs=isa/<id>`, with optional edition and version segments.
213
+ `?fhir_vs=ecl/<expression>` is not recognised; use a `constraint` filter.
214
+ - Implicit concept maps (`?fhir_cm=<id>`): only the four historical association
215
+ reference sets (POSSIBLY EQUIVALENT TO, REPLACED BY, SAME AS, ALTERNATIVE).
216
+ Simple and extended map reference sets (ICD-10, ICD-O, ...) are loaded but
217
+ are not exposed as implicit concept maps, and reverse translation is not
218
+ supported.
219
+ - Edition to edition translation (`<edition>/version/<date>?fhir_cm=edition/<module>`):
220
+ a `$translate` from an edition to one it depends on (per the module dependency
221
+ reference set) needs no map - the answer comes from the definitions. A concept in
222
+ the target edition's modules is its own translation; an inactive concept translates
223
+ through its historical associations; a fully defined extension concept translates
224
+ to its normal form (or to the target concept with that normal form, if there is
225
+ one), provided the normal form uses only target edition content; anything else
226
+ translates to its proximal target-edition ancestors as `source-is-narrower-than-target`,
227
+ unless the only such ancestors are top-level hierarchies, which is reported as a
228
+ reason for no translation. The map is found from `targetSystem` =
229
+ `http://snomed.info/sct|<target edition[/version/date]>`, or nominated by url.
230
+ - `$closure` works through `subsumesTest`, so adding a post-coordinated
231
+ expression to a closure table hits the `cannot-determine` rule above whenever
232
+ it is unrelated to an existing entry.
233
+
234
+ ### Text search
235
+
236
+ - The `filter` parameter on `$expand` uses an in-memory word index built the
237
+ first time an edition is searched (`tx/sct/text-index.js`): every token of
238
+ every active description, matched by prefix and by Porter stem. The stemmer
239
+ is English, so in other languages only prefix matching helps. Inactive
240
+ descriptions are not indexed.
241
+
242
+ ### Known data quality traps
243
+
244
+ - The three copies of the tx-ecosystem SNOMED test edition
245
+ (`data/terminology-cache/sct_test_*.cache`, `tx/data/snomed-testing.cache`,
246
+ `data/snomed-testing.cache`) drift. The committed jest cache is older than
247
+ the RF2 subset in `tx/data/snomed` and lacks module ids, definition status
248
+ and some concepts; rebuilding it changes test expectations.
249
+ - `tx.fhir.org` (the Java server) and FHIRsmith disagree on a few expected
250
+ results in the tx-ecosystem suite (display selection, MRCM); the suite
251
+ records FHIRsmith's reading where Grahame has decided it.