fhirsmith 0.18.1 → 1.0.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 (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -12,6 +12,7 @@ const {
12
12
  SnomedRelationshipList, SnomedReferenceSetMembers, SnomedReferenceSetIndex
13
13
  } = require('../sct/structures');
14
14
  const {SnomedExpressionServices} = require("../sct/expressions");
15
+ const terminal = require('./terminal');
15
16
 
16
17
  // definitionStatusId values from the RF2 concept file
17
18
  const SNOMED_DEFINITION_STATUS_PRIMITIVE = 900000000000074008n;
@@ -106,16 +107,16 @@ class SnomedModule extends BaseTerminologyModule {
106
107
  } catch (error) {
107
108
  this.logError(`Import command failed: ${error.message}`);
108
109
  if (options.verbose) {
109
- console.error(error.stack);
110
+ terminal.error(error.stack);
110
111
  }
111
112
  throw error;
112
113
  }
113
114
  }
114
115
 
115
116
  async confirmImport(config) {
116
- console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
117
- console.log(` Source: ${chalk.white(config.source)}`);
118
- console.log(` Destination: ${chalk.white(config.dest)}`);
117
+ terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
118
+ terminal.log(` Source: ${chalk.white(config.source)}`);
119
+ terminal.log(` Destination: ${chalk.white(config.dest)}`);
119
120
 
120
121
  if (config.edition) {
121
122
  const editions = {
@@ -125,26 +126,26 @@ class SnomedModule extends BaseTerminologyModule {
125
126
  // ... other editions
126
127
  };
127
128
  const editionName = editions[config.edition] || `Edition ${config.edition}`;
128
- console.log(` Edition: ${chalk.white(editionName)} (${config.edition})`);
129
+ terminal.log(` Edition: ${chalk.white(editionName)} (${config.edition})`);
129
130
  }
130
131
 
131
132
  if (config.version) {
132
- console.log(` Version: ${chalk.white(config.version)}`);
133
+ terminal.log(` Version: ${chalk.white(config.version)}`);
133
134
  }
134
135
 
135
136
  if (config.uri) {
136
- console.log(` Version URI: ${chalk.white(config.uri)}`);
137
+ terminal.log(` Version URI: ${chalk.white(config.uri)}`);
137
138
  }
138
139
 
139
140
  if (config.language) {
140
- console.log(` Language: ${chalk.white(config.language)}`);
141
+ terminal.log(` Language: ${chalk.white(config.language)}`);
141
142
  }
142
143
 
143
- console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
144
- console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
144
+ terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
145
+ terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
145
146
 
146
147
  if (config.estimatedDuration) {
147
- console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
148
+ terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
148
149
  }
149
150
 
150
151
  const { confirmed } = await inquirer.prompt({
@@ -357,7 +358,7 @@ class SnomedModule extends BaseTerminologyModule {
357
358
  } catch (error) {
358
359
  // Silent fail - auto-detection is best-effort
359
360
  if (this.config?.verbose) {
360
- console.log(`Auto-detection warning: ${error.message}`);
361
+ terminal.log(`Auto-detection warning: ${error.message}`);
361
362
  }
362
363
  }
363
364
 
@@ -661,7 +662,7 @@ class SnomedModule extends BaseTerminologyModule {
661
662
 
662
663
  async runImportWithoutConfigSaving(config) {
663
664
  try {
664
- console.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
665
+ terminal.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
665
666
 
666
667
  // Pre-flight checks
667
668
  this.logInfo('Running pre-flight checks...');
@@ -680,7 +681,7 @@ class SnomedModule extends BaseTerminologyModule {
680
681
  this.stopProgress();
681
682
  this.logError(`${this.getName()} import failed: ${error.message}`);
682
683
  if (config.verbose) {
683
- console.error(error.stack);
684
+ terminal.error(error.stack);
684
685
  }
685
686
  process.exit(1);
686
687
  }
@@ -816,10 +817,10 @@ class SnomedModule extends BaseTerminologyModule {
816
817
  const stats = await this.validateSnomedDirectory(options.source);
817
818
 
818
819
  this.logSuccess('Directory validation passed');
819
- console.log(` Concept files: ${stats.conceptFiles}`);
820
- console.log(` Description files: ${stats.descriptionFiles}`);
821
- console.log(` Relationship files: ${stats.relationshipFiles}`);
822
- console.log(` Refset directories: ${stats.refsetDirectories}`);
820
+ terminal.log(` Concept files: ${stats.conceptFiles}`);
821
+ terminal.log(` Description files: ${stats.descriptionFiles}`);
822
+ terminal.log(` Relationship files: ${stats.relationshipFiles}`);
823
+ terminal.log(` Refset directories: ${stats.refsetDirectories}`);
823
824
 
824
825
  } catch (error) {
825
826
  this.logError(`Validation failed: ${error.message}`);
@@ -843,26 +844,26 @@ class SnomedModule extends BaseTerminologyModule {
843
844
  const data = await reader.loadSnomedData();
844
845
 
845
846
  this.logSuccess('Cache file status:');
846
- console.log(` Cache Version: ${data.cacheVersion}`);
847
- console.log(` Version URI: ${data.versionUri}`);
848
- console.log(` Version Date: ${data.versionDate}`);
849
- console.log(` Edition: ${data.edition}`);
850
- console.log(` SNOMED Version: ${data.version}`);
847
+ terminal.log(` Cache Version: ${data.cacheVersion}`);
848
+ terminal.log(` Version URI: ${data.versionUri}`);
849
+ terminal.log(` Version Date: ${data.versionDate}`);
850
+ terminal.log(` Edition: ${data.edition}`);
851
+ terminal.log(` SNOMED Version: ${data.version}`);
851
852
 
852
853
  // Create structure instances to get counts
853
854
  const concepts = new SnomedConceptList(data.concept);
854
855
  const descriptions = new SnomedDescriptions(data.desc);
855
856
  const relationships = new SnomedRelationshipList(data.rel);
856
857
 
857
- console.log(` Concepts: ${concepts.count().toLocaleString()}`);
858
- console.log(` Descriptions: ${descriptions.count().toLocaleString()}`);
859
- console.log(` Relationships: ${relationships.count().toLocaleString()}`);
860
- console.log(` Active Roots: ${data.activeRoots.length}`);
861
- console.log(` Inactive Roots: ${data.inactiveRoots.length}`);
858
+ terminal.log(` Concepts: ${concepts.count().toLocaleString()}`);
859
+ terminal.log(` Descriptions: ${descriptions.count().toLocaleString()}`);
860
+ terminal.log(` Relationships: ${relationships.count().toLocaleString()}`);
861
+ terminal.log(` Active Roots: ${data.activeRoots.length}`);
862
+ terminal.log(` Inactive Roots: ${data.inactiveRoots.length}`);
862
863
 
863
864
  const fileStat = fs.statSync(cachePath);
864
- console.log(` File Size: ${(fileStat.size / (1024 * 1024 * 1024)).toFixed(2)} GB`);
865
- console.log(` Last Modified: ${fileStat.mtime.toISOString()}`);
865
+ terminal.log(` File Size: ${(fileStat.size / (1024 * 1024 * 1024)).toFixed(2)} GB`);
866
+ terminal.log(` Last Modified: ${fileStat.mtime.toISOString()}`);
866
867
 
867
868
  } catch (error) {
868
869
  this.logError(`Status check failed: ${error.message}`);
@@ -924,7 +925,7 @@ class SnomedImporterWithProgress {
924
925
  message += ` (${rate.toLocaleString()} items/sec)`;
925
926
  }
926
927
 
927
- console.log(message);
928
+ terminal.log(message);
928
929
 
929
930
  // Clean up
930
931
  this.taskStartTimes.delete(taskName);
@@ -1139,6 +1140,9 @@ class SnomedImporter {
1139
1140
  // Step 10a: Read alternate identifiers (e.g. the LOINC codes of the LOINC Extension)
1140
1141
  await this.readIdentifiers();
1141
1142
 
1143
+ // Step 10b: Read concrete value relationships (sct2_RelationshipConcreteValues)
1144
+ await this.readConcreteValues();
1145
+
1142
1146
  // Step 11: Link concepts
1143
1147
  this.linkConcepts();
1144
1148
 
@@ -1158,7 +1162,7 @@ class SnomedImporter {
1158
1162
  await this.saveCache();
1159
1163
 
1160
1164
  } catch (error) {
1161
- console.error('DEBUG: Import failed with error:', error);
1165
+ terminal.error('DEBUG: Import failed with error:', error);
1162
1166
  throw new Error(`Import failed: ${error.message}`);
1163
1167
  }
1164
1168
  }
@@ -1169,6 +1173,7 @@ class SnomedImporter {
1169
1173
  descriptions: [],
1170
1174
  relationships: [],
1171
1175
  identifiers: [],
1176
+ concreteValues: [],
1172
1177
  refsetDirectories: []
1173
1178
  };
1174
1179
 
@@ -1177,7 +1182,7 @@ class SnomedImporter {
1177
1182
  // extension content is layered on top.
1178
1183
  if (this.config.base) {
1179
1184
  if (this.config.verbose) {
1180
- console.log(`Loading base edition from: ${this.config.base}`);
1185
+ terminal.log(`Loading base edition from: ${this.config.base}`);
1181
1186
  }
1182
1187
  this._scanDirectory(this.config.base, files);
1183
1188
  }
@@ -1224,9 +1229,11 @@ class SnomedImporter {
1224
1229
  files.relationships.push(filePath);
1225
1230
  } else if (files.identifiers && firstLine.startsWith('alternateIdentifier\teffectiveTime\tactive\tmoduleId\tidentifierSchemeId\treferencedComponentId')) {
1226
1231
  files.identifiers.push(filePath);
1232
+ } else if (files.concreteValues && firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tvalue\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId')) {
1233
+ files.concreteValues.push(filePath);
1227
1234
  }
1228
1235
  } catch (error) {
1229
- console.log(`DEBUG: Error reading file ${filePath}: ${error.message}`);
1236
+ terminal.log(`DEBUG: Error reading file ${filePath}: ${error.message}`);
1230
1237
  }
1231
1238
  }
1232
1239
 
@@ -1522,14 +1529,15 @@ class SnomedImporter {
1522
1529
  const month = parseInt(dateStr.substring(4, 6));
1523
1530
  const day = parseInt(dateStr.substring(6, 8));
1524
1531
 
1525
- // Create target date
1526
- const targetDate = new Date(year, month - 1, day);
1527
-
1528
- // Pascal TDateTime epoch: December 30, 1899
1529
- const pascalEpoch = new Date(1899, 11, 30); // Month is 0-based in JS
1532
+ // Days since the Pascal TDateTime epoch (December 30, 1899), counted in UTC. Local dates are
1533
+ // not whole days apart: 1899 local time is the zone's historical mean time (e.g. +9:39:52 in
1534
+ // Melbourne) and today's has daylight saving, so the difference came out a day short in zones
1535
+ // ahead of UTC, and every effectiveTime in a cache built there was a day early
1536
+ const targetDate = Date.UTC(year, month - 1, day);
1537
+ const pascalEpoch = Date.UTC(1899, 11, 30); // Month is 0-based in JS
1530
1538
 
1531
1539
  // Calculate days difference
1532
- const daysDiff = Math.floor((targetDate - pascalEpoch) / (1000 * 60 * 60 * 24));
1540
+ const daysDiff = Math.round((targetDate - pascalEpoch) / (1000 * 60 * 60 * 24));
1533
1541
 
1534
1542
  // Ensure it fits in 16 bits (0-65535) and is positive
1535
1543
  if (daysDiff < 0 || daysDiff > 65535) {
@@ -1778,7 +1786,59 @@ class SnomedImporter {
1778
1786
  }
1779
1787
  this.identifiers.sort((a, b) => (a.join('|') < b.join('|') ? -1 : 1));
1780
1788
  if (this.identifiers.length > 0 || missing > 0) {
1781
- console.log(`Alternate identifiers: ${this.identifiers.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
1789
+ terminal.log(`Alternate identifiers: ${this.identifiers.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
1790
+ }
1791
+ }
1792
+
1793
+ /**
1794
+ * Read the concrete value relationship files (sct2_RelationshipConcreteValues_*):
1795
+ * the numeric and string attribute values of concepts (strength, count of base of
1796
+ * active ingredient, ...). Kept as rows [sourceId, typeId, group, value] - the value
1797
+ * as written in RF2 ("#500", "#5.5", "\"text\"", "true") - for the ECL evaluator;
1798
+ * they are not relationships to concepts, so they do not join the relationship
1799
+ * table. Only active, inferred rows; where a row appears more than once the latest
1800
+ * effectiveTime wins.
1801
+ */
1802
+ async readConcreteValues() {
1803
+ const rows = new Map(); // id -> {effectiveTime, active, row}
1804
+ for (const file of this.files.concreteValues || []) {
1805
+ const rl = readline.createInterface({
1806
+ input: fs.createReadStream(file),
1807
+ crlfDelay: Infinity
1808
+ });
1809
+ let lineCount = 0;
1810
+ for await (const line of rl) {
1811
+ lineCount++;
1812
+ if (lineCount === 1 || !line) continue;
1813
+ const parts = line.split('\t');
1814
+ if (parts.length < 10) continue;
1815
+ const [id, effectiveTime, active, , sourceId, value, group, typeId, characteristicTypeId] = parts.map(p => p.trim());
1816
+ if (characteristicTypeId !== '900000000000011006') continue; // inferred only
1817
+ const prior = rows.get(id);
1818
+ if (!prior || prior.effectiveTime <= effectiveTime) {
1819
+ rows.set(id, { effectiveTime, active: active === '1', row: [sourceId, typeId, parseInt(group, 10) || 0, value] });
1820
+ }
1821
+ }
1822
+ }
1823
+ this.concreteValues = [];
1824
+ let missing = 0;
1825
+ for (const r of rows.values()) {
1826
+ if (!r.active) continue;
1827
+ let known = false;
1828
+ try {
1829
+ known = this.conceptMap.has(BigInt(r.row[0])) && this.conceptMap.has(BigInt(r.row[1]));
1830
+ } catch {
1831
+ known = false;
1832
+ }
1833
+ if (known) {
1834
+ this.concreteValues.push(r.row);
1835
+ } else {
1836
+ missing++;
1837
+ }
1838
+ }
1839
+ this.concreteValues.sort((a, b) => (a.join('|') < b.join('|') ? -1 : 1));
1840
+ if (this.concreteValues.length > 0 || missing > 0) {
1841
+ terminal.log(`Concrete values: ${this.concreteValues.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
1782
1842
  }
1783
1843
  }
1784
1844
 
@@ -2193,7 +2253,7 @@ class SnomedImporter {
2193
2253
 
2194
2254
  async processRefsets() {
2195
2255
  if (this.files.refsetDirectories.length === 0) {
2196
- console.log('No reference set directories found');
2256
+ terminal.log('No reference set directories found');
2197
2257
  this.refsetIndex.doneBuild();
2198
2258
  this.refsetMembers.doneBuild();
2199
2259
  return;
@@ -2309,7 +2369,7 @@ class SnomedImporter {
2309
2369
  const parts = fileName.split('_');
2310
2370
 
2311
2371
  if (parts.length < 3) {
2312
- console.log(`Skipping file with unexpected name format: ${fileName}`);
2372
+ terminal.log(`Skipping file with unexpected name format: ${fileName}`);
2313
2373
  return;
2314
2374
  }
2315
2375
 
@@ -2333,19 +2393,16 @@ class SnomedImporter {
2333
2393
  lineNumber++;
2334
2394
 
2335
2395
  if (lineNumber === 1) {
2336
- // Parse headers
2396
+ // Parse headers. Map reference sets (simple, complex, extended) are loaded like
2397
+ // any other: ECL ^ and ?fhir_vs=refset/ can name any reference set, and the
2398
+ // ICD-10 maps are among the most used.
2337
2399
  headers = line.split('\t').map(h => h.trim());
2338
-
2339
- // Skip if this looks like a mapping reference set
2340
- if (line.toLowerCase().includes('map')) {
2341
- return;
2342
- }
2343
2400
  continue;
2344
2401
  }
2345
2402
 
2346
2403
  const fields = line.split('\t');
2347
2404
  if (fields.length < 6) {
2348
- console.log(`Skipping line ${lineNumber} with insufficient fields: ${fields.length}`);
2405
+ terminal.log(`Skipping line ${lineNumber} with insufficient fields: ${fields.length}`);
2349
2406
  continue; // Minimum fields required
2350
2407
  }
2351
2408
 
@@ -2381,14 +2438,14 @@ class SnomedImporter {
2381
2438
  try {
2382
2439
  member.id = this.parseGUID(id);
2383
2440
  } catch (error) {
2384
- console.log(`Invalid GUID in ${fileName}: ${id}`);
2441
+ terminal.log(`Invalid GUID in ${fileName}: ${id}`);
2385
2442
  continue;
2386
2443
  }
2387
2444
 
2388
2445
  // Find module concept
2389
2446
  const moduleConcept = this.conceptMap.get(BigInt(moduleId));
2390
2447
  if (!moduleConcept) {
2391
- console.log(`Module concept not found: ${moduleId}`);
2448
+ terminal.log(`Module concept not found: ${moduleId}`);
2392
2449
  continue;
2393
2450
  }
2394
2451
  member.module = moduleConcept.index;
@@ -2421,7 +2478,7 @@ class SnomedImporter {
2421
2478
  // Try to find as relationship (simplified - would need relationship ID lookup)
2422
2479
  member.kind = 3; // Other/unknown
2423
2480
  member.ref = 0;
2424
- console.log(`Component not found: ${referencedComponentId}`);
2481
+ terminal.log(`Component not found: ${referencedComponentId}`);
2425
2482
  continue;
2426
2483
  }
2427
2484
  }
@@ -2444,7 +2501,7 @@ class SnomedImporter {
2444
2501
  }
2445
2502
 
2446
2503
  } catch (error) {
2447
- console.error(`Error processing reference set ${filePath}:`, error);
2504
+ terminal.error(`Error processing reference set ${filePath}:`, error);
2448
2505
  }
2449
2506
  }
2450
2507
 
@@ -2532,7 +2589,7 @@ class SnomedImporter {
2532
2589
  value = descResult.index;
2533
2590
  type = 2; // Description
2534
2591
  } else {
2535
- console.log(`Referenced component not found: ${field}`);
2592
+ terminal.log(`Referenced component not found: ${field}`);
2536
2593
  value = 0;
2537
2594
  type = 1;
2538
2595
  }
@@ -2789,7 +2846,7 @@ class SnomedImporter {
2789
2846
  } catch (error) {
2790
2847
  // Log the error but continue processing other concepts
2791
2848
  if (this.config.verbose) {
2792
- console.warn(`Warning: Could not build normal form for concept ${concept.id}: ${error.message}`);
2849
+ terminal.warn(`Warning: Could not build normal form for concept ${concept.id}: ${error.message}`);
2793
2850
  }
2794
2851
  }
2795
2852
 
@@ -2804,7 +2861,7 @@ class SnomedImporter {
2804
2861
  }
2805
2862
 
2806
2863
  if (this.config.verbose) {
2807
- console.log(`Normal forms: ${normalFormsAdded} concepts have non-trivial normal forms`);
2864
+ terminal.log(`Normal forms: ${normalFormsAdded} concepts have non-trivial normal forms`);
2808
2865
  }
2809
2866
  }
2810
2867
 
@@ -2823,11 +2880,14 @@ class SnomedImporter {
2823
2880
  progressBar?.update(50);
2824
2881
 
2825
2882
  const identifiers = this.identifiers || [];
2883
+ const concreteValues = this.concreteValues || [];
2826
2884
  await writer.writeCache({
2827
- // 18 = 17 plus the alternate identifiers. Only used when there are some, so that
2828
- // caches without them stay readable by servers that only know 17
2829
- version: identifiers.length > 0 ? '18' : '17',
2885
+ // 17 = the base format; 18 = 17 plus the alternate identifiers; 19 = 18 plus
2886
+ // the concrete value relationships. Each is only used when it has something
2887
+ // to carry, so that caches without them stay readable by older servers.
2888
+ version: concreteValues.length > 0 ? '19' : identifiers.length > 0 ? '18' : '17',
2830
2889
  identifiers: identifiers,
2890
+ concreteValues: concreteValues,
2831
2891
  versionUri: this.isTesting ? this.config.uri.replace("/sct/", "/xsct/") : this.config.uri,
2832
2892
  versionDate: this.extractDateFromUri(this.config.uri),
2833
2893
 
@@ -2914,10 +2974,14 @@ class SnomedCacheWriter {
2914
2974
  }
2915
2975
  buffers.push(this.writeInteger(data.defaultLanguage));
2916
2976
 
2917
- if (data.version === '18') {
2977
+ if (data.version === '18' || data.version === '19') {
2918
2978
  // alternate identifiers, as JSON: [[scheme, module, code, conceptId], ...]
2919
2979
  buffers.push(this.writeBytes(Buffer.from(JSON.stringify(data.identifiers || []), 'utf8')));
2920
2980
  }
2981
+ if (data.version === '19') {
2982
+ // concrete value relationships, as JSON: [[sourceId, typeId, group, value], ...]
2983
+ buffers.push(this.writeBytes(Buffer.from(JSON.stringify(data.concreteValues || []), 'utf8')));
2984
+ }
2921
2985
 
2922
2986
  // Write to file
2923
2987
  const finalBuffer = Buffer.concat(buffers);
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const sqlite3 = require('sqlite3').verbose();
4
4
  const path = require("path");
5
5
  const readline = require("readline");
6
+ const terminal = require('./terminal');
6
7
 
7
8
  class UniiModule extends BaseTerminologyModule {
8
9
  getName() {
@@ -81,16 +82,16 @@ class UniiModule extends BaseTerminologyModule {
81
82
  const inquirer = require('inquirer');
82
83
  const chalk = require('chalk');
83
84
 
84
- console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
85
- console.log(` Source: ${chalk.white(config.source)}`);
86
- console.log(` Destination: ${chalk.white(config.dest)}`);
87
- console.log(` Version: ${chalk.white(config.version)}`);
88
- console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
89
- console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
90
- console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
85
+ terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
86
+ terminal.log(` Source: ${chalk.white(config.source)}`);
87
+ terminal.log(` Destination: ${chalk.white(config.dest)}`);
88
+ terminal.log(` Version: ${chalk.white(config.version)}`);
89
+ terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
90
+ terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
91
+ terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
91
92
 
92
93
  if (config.estimatedDuration) {
93
- console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
94
+ terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
94
95
  }
95
96
 
96
97
  const { confirmed } = await inquirer.prompt({
@@ -120,14 +121,14 @@ class UniiModule extends BaseTerminologyModule {
120
121
  const stats = await this.validateUniiFile(options.source, parseInt(options.sample));
121
122
 
122
123
  this.logSuccess('File validation passed');
123
- console.log(` Lines: ${stats.totalLines}`);
124
- console.log(` Estimated UNII codes: ${stats.estimatedCodes}`);
125
- console.log(` Estimated descriptions: ${stats.estimatedDescriptions}`);
126
- console.log(` Sample data format: ${stats.formatValid ? 'Valid' : 'Invalid'}`);
124
+ terminal.log(` Lines: ${stats.totalLines}`);
125
+ terminal.log(` Estimated UNII codes: ${stats.estimatedCodes}`);
126
+ terminal.log(` Estimated descriptions: ${stats.estimatedDescriptions}`);
127
+ terminal.log(` Sample data format: ${stats.formatValid ? 'Valid' : 'Invalid'}`);
127
128
 
128
129
  if (stats.warnings.length > 0) {
129
130
  this.logWarning('Validation warnings:');
130
- stats.warnings.forEach(warning => console.log(` ${warning}`));
131
+ stats.warnings.forEach(warning => terminal.log(` ${warning}`));
131
132
  }
132
133
 
133
134
  } catch (error) {
@@ -149,11 +150,11 @@ class UniiModule extends BaseTerminologyModule {
149
150
  const stats = await this.getDatabaseStats(dbPath);
150
151
 
151
152
  this.logSuccess('Database status:');
152
- console.log(` Version: ${stats.version}`);
153
- console.log(` UNII Codes: ${stats.uniiCount.toLocaleString()}`);
154
- console.log(` Descriptions: ${stats.descCount.toLocaleString()}`);
155
- console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
156
- console.log(` Last Modified: ${stats.lastModified}`);
153
+ terminal.log(` Version: ${stats.version}`);
154
+ terminal.log(` UNII Codes: ${stats.uniiCount.toLocaleString()}`);
155
+ terminal.log(` Descriptions: ${stats.descCount.toLocaleString()}`);
156
+ terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
157
+ terminal.log(` Last Modified: ${stats.lastModified}`);
157
158
 
158
159
  } catch (error) {
159
160
  this.logError(`Status check failed: ${error.message}`);
@@ -304,7 +305,7 @@ class UniiModule extends BaseTerminologyModule {
304
305
  db.serialize(() => {
305
306
  indexes.forEach(sql => {
306
307
  db.run(sql, (err) => {
307
- if (err) console.warn(`Index creation warning: ${err.message}`);
308
+ if (err) terminal.warn(`Index creation warning: ${err.message}`);
308
309
  });
309
310
  });
310
311
  });
@@ -327,7 +328,7 @@ class UniiDataMigrator {
327
328
  * @returns {Promise<void>}
328
329
  */
329
330
  async migrate(sourceFile, destFile, version = 'unknown', verbose = true) {
330
- if (verbose) console.log('Starting UNII data migration...');
331
+ if (verbose) terminal.log('Starting UNII data migration...');
331
332
 
332
333
  // Ensure destination directory exists
333
334
  const destDir = path.dirname(destFile);
@@ -350,7 +351,7 @@ class UniiDataMigrator {
350
351
  // Process source file
351
352
  await this.#processSourceFile(db, sourceFile, verbose);
352
353
 
353
- if (verbose) console.log('UNII data migration completed successfully');
354
+ if (verbose) terminal.log('UNII data migration completed successfully');
354
355
  } finally {
355
356
  await this.#closeDatabase(db, verbose);
356
357
  }
@@ -361,7 +362,7 @@ class UniiDataMigrator {
361
362
  * @private
362
363
  */
363
364
  async #createTables(db, version, verbose = true) {
364
- if (verbose) console.log('Creating database tables...');
365
+ if (verbose) terminal.log('Creating database tables...');
365
366
 
366
367
  return new Promise((resolve, reject) => {
367
368
  db.serialize(() => {
@@ -400,7 +401,7 @@ class UniiDataMigrator {
400
401
  // Insert version
401
402
  db.run('INSERT INTO UniiVersion (Version) VALUES (?)', [version], (err) => {
402
403
  if (err) return reject(err);
403
- if (verbose) console.log('Database tables created');
404
+ if (verbose) terminal.log('Database tables created');
404
405
  resolve();
405
406
  });
406
407
  });
@@ -412,7 +413,7 @@ class UniiDataMigrator {
412
413
  * @private
413
414
  */
414
415
  async #processSourceFile(db, sourceFile, verbose = true) {
415
- if (verbose) console.log('Processing source file:', sourceFile);
416
+ if (verbose) terminal.log('Processing source file:', sourceFile);
416
417
 
417
418
  if (!fs.existsSync(sourceFile)) {
418
419
  throw new Error(`Source file not found: ${sourceFile}`);
@@ -438,7 +439,7 @@ class UniiDataMigrator {
438
439
  throw new Error('Source file is empty');
439
440
  }
440
441
 
441
- if (verbose) console.log(`Read ${lines.length} lines, processing...`);
442
+ if (verbose) terminal.log(`Read ${lines.length} lines, processing...`);
442
443
 
443
444
  // Track processed codes and auto-increment keys
444
445
  const codeMap = new Map(); // code -> UniiKey
@@ -467,7 +468,7 @@ class UniiDataMigrator {
467
468
 
468
469
  // Need at least 3 columns (Display, Type, Code)
469
470
  if (cols.length < 3) {
470
- if (verbose) console.warn(`Skipping line ${i + 1}: insufficient columns`);
471
+ if (verbose) terminal.warn(`Skipping line ${i + 1}: insufficient columns`);
471
472
  continue;
472
473
  }
473
474
 
@@ -476,7 +477,7 @@ class UniiDataMigrator {
476
477
  const code = cols[2] || '';
477
478
 
478
479
  if (!code) {
479
- if (verbose) console.warn(`Skipping line ${i + 1}: empty UNII code`);
480
+ if (verbose) terminal.warn(`Skipping line ${i + 1}: empty UNII code`);
480
481
  continue;
481
482
  }
482
483
 
@@ -510,14 +511,14 @@ class UniiDataMigrator {
510
511
  });
511
512
 
512
513
  if (processedLines % 10000 === 0) {
513
- console.log(`Processed ${processedLines} lines...`);
514
+ terminal.log(`Processed ${processedLines} lines...`);
514
515
  }
515
516
  }
516
517
 
517
518
  if (verbose) {
518
- console.log(`Processing completed. Total lines processed: ${processedLines}`);
519
- console.log(`Unique UNII codes: ${codeMap.size}`);
520
- console.log(`Total descriptions: ${lastUniiDescKey}`);
519
+ terminal.log(`Processing completed. Total lines processed: ${processedLines}`);
520
+ terminal.log(`Unique UNII codes: ${codeMap.size}`);
521
+ terminal.log(`Total descriptions: ${lastUniiDescKey}`);
521
522
  }
522
523
  }
523
524
 
@@ -529,7 +530,7 @@ class UniiDataMigrator {
529
530
  return new Promise((resolve) => {
530
531
  db.close((err) => {
531
532
  if (err && verbose) {
532
- console.error('Error closing database:', err);
533
+ terminal.error('Error closing database:', err);
533
534
  }
534
535
  resolve();
535
536
  });
@@ -558,8 +559,8 @@ class UniiDataMigratorWithProgress {
558
559
  let processedLines = 0;
559
560
 
560
561
  // Override the progress reporting in the original migrator
561
- const originalConsoleLog = console.log;
562
- console.log = (...args) => {
562
+ const originalLog = terminal.log;
563
+ terminal.log = (...args) => {
563
564
  const message = args.join(' ');
564
565
  if (message.includes('Processed') && message.includes('lines')) {
565
566
  const match = message.match(/Processed (\d+) lines/);
@@ -568,13 +569,13 @@ class UniiDataMigratorWithProgress {
568
569
  this.module.updateProgress(processedLines);
569
570
  }
570
571
  }
571
- if (verbose) originalConsoleLog(...args);
572
+ if (verbose) originalLog(...args);
572
573
  };
573
574
 
574
575
  try {
575
576
  await originalMigrator.migrate(sourceFile, destFile, version, verbose);
576
577
  } finally {
577
- console.log = originalConsoleLog;
578
+ terminal.log = originalLog;
578
579
  this.module.stopProgress();
579
580
  }
580
581
  }