fhirsmith 0.3.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 (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. package/xig/xig.js +3049 -0
@@ -0,0 +1,2457 @@
1
+ const { BaseTerminologyModule } = require('./tx-import-base');
2
+ const fs = require('fs');
3
+ const inquirer = require('inquirer');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const chalk = require('chalk');
7
+ const natural = require('natural');
8
+
9
+ const {
10
+ SnomedStrings, SnomedWords, SnomedStems, SnomedReferences,
11
+ SnomedDescriptions, SnomedDescriptionIndex, SnomedConceptList,
12
+ SnomedRelationshipList, SnomedReferenceSetMembers, SnomedReferenceSetIndex
13
+ } = require('../sct/structures');
14
+ const {SnomedExpressionServices} = require("../sct/expressions");
15
+
16
+ class SnomedModule extends BaseTerminologyModule {
17
+
18
+ constructor() {
19
+ super();
20
+ }
21
+
22
+ getName() {
23
+ return 'snomed';
24
+ }
25
+
26
+ getDescription() {
27
+ return 'SNOMED Clinical Terms (SNOMED CT) from IHTSDO';
28
+ }
29
+
30
+ getSupportedFormats() {
31
+ return ['rf2', 'directory'];
32
+ }
33
+
34
+ getDefaultConfig() {
35
+ return {
36
+ verbose: true,
37
+ overwrite: false,
38
+ createIndexes: true,
39
+ language: 'en-US',
40
+ dest: './data/snomed.cache'
41
+ };
42
+ }
43
+
44
+ getEstimatedDuration() {
45
+ return '2-6 hours (depending on edition size)';
46
+ }
47
+
48
+ registerCommands(terminologyCommand, globalOptions) {
49
+ // Import command
50
+ terminologyCommand
51
+ .command('import')
52
+ .description('Import SNOMED CT data from RF2 source directory')
53
+ .option('-s, --source <directory>', 'Source directory containing RF2 files')
54
+ .option('-b, --base <directory>', 'Base edition directory (for extensions)')
55
+ .option('-d, --dest <file>', 'Destination cache file')
56
+ .option('-e, --edition <code>', 'Edition code (e.g., 900000000000207008 for International)')
57
+ .option('-v, --version <version>', 'Version in YYYYMMDD format (e.g., 20250801)')
58
+ .option('-u, --uri <uri>', 'Version URI (overrides edition/version if provided)')
59
+ .option('-l, --language <code>', 'Default language code (overrides edition default if provided)')
60
+ .option('-y, --yes', 'Skip confirmations')
61
+ .action(async (options) => {
62
+ await this.handleImportCommand({...globalOptions, ...options});
63
+ });
64
+
65
+ // Validate command
66
+ terminologyCommand
67
+ .command('validate')
68
+ .description('Validate SNOMED CT RF2 directory structure')
69
+ .option('-s, --source <directory>', 'Source directory to validate')
70
+ .action(async (options) => {
71
+ await this.handleValidateCommand({...globalOptions, ...options});
72
+ });
73
+
74
+ // Status command
75
+ terminologyCommand
76
+ .command('status')
77
+ .description('Show status of SNOMED CT cache')
78
+ .option('-d, --dest <file>', 'Cache file to check')
79
+ .action(async (options) => {
80
+ await this.handleStatusCommand({...globalOptions, ...options});
81
+ });
82
+ }
83
+
84
+ async handleImportCommand(options) {
85
+ try {
86
+ // Gather configuration
87
+ const config = await this.gatherSnomedConfig(options);
88
+
89
+ // Show confirmation unless --yes is specified
90
+ if (!options.yes) {
91
+ const confirmed = await this.confirmImport(config);
92
+ if (!confirmed) {
93
+ this.logInfo('Import cancelled');
94
+ return;
95
+ }
96
+ }
97
+
98
+ // Save configuration immediately after confirmation
99
+ this.rememberSuccessfulConfig(config);
100
+
101
+ // Run the import
102
+ await this.runImportWithoutConfigSaving(config);
103
+ } catch (error) {
104
+ this.logError(`Import command failed: ${error.message}`);
105
+ if (options.verbose) {
106
+ console.error(error.stack);
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ async confirmImport(config) {
113
+ console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
114
+ console.log(` Source: ${chalk.white(config.source)}`);
115
+ console.log(` Destination: ${chalk.white(config.dest)}`);
116
+
117
+ if (config.edition) {
118
+ const editions = {
119
+ "900000000000207008": "International",
120
+ "731000124108": "US Edition",
121
+ "32506021000036107": "Australian Edition",
122
+ // ... other editions
123
+ };
124
+ const editionName = editions[config.edition] || `Edition ${config.edition}`;
125
+ console.log(` Edition: ${chalk.white(editionName)} (${config.edition})`);
126
+ }
127
+
128
+ if (config.version) {
129
+ console.log(` Version: ${chalk.white(config.version)}`);
130
+ }
131
+
132
+ if (config.uri) {
133
+ console.log(` Version URI: ${chalk.white(config.uri)}`);
134
+ }
135
+
136
+ if (config.language) {
137
+ console.log(` Language: ${chalk.white(config.language)}`);
138
+ }
139
+
140
+ console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
141
+ console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
142
+
143
+ if (config.estimatedDuration) {
144
+ console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
145
+ }
146
+
147
+ const { confirmed } = await inquirer.prompt({
148
+ type: 'confirm',
149
+ name: 'confirmed',
150
+ message: 'Proceed with import?',
151
+ default: true
152
+ });
153
+
154
+ return confirmed;
155
+ }
156
+
157
+ async gatherSnomedConfig(options) {
158
+ const baseConfig = await this.gatherCommonConfig(options);
159
+
160
+ const editions = {
161
+ "900000000000207008": { name: "International", needsBase: false, lang: "en-US" },
162
+ "731000124108": { name: "US Edition", needsBase: false, lang: "en-US" },
163
+ "32506021000036107": { name: "Australian Edition", needsBase: true, lang: "en-AU" },
164
+ "449081005": { name: "Spanish Edition (International)", needsBase: true, lang: "es" },
165
+ "11000279109": { name: "Czech Edition", needsBase: false, lang: "cs-CZ" },
166
+ "554471000005108": { name: "Danish Edition", needsBase: true, lang: "da-DK" },
167
+ "11000146104": { name: "Dutch Edition", needsBase: true, lang: "nl-NL" },
168
+ "45991000052106": { name: "Swedish Edition", needsBase: true, lang: "sv-SE" },
169
+ "83821000000107": { name: "UK Edition", needsBase: true, lang: "en-GB" },
170
+ "11000172109": { name: "Belgian Edition", needsBase: true, lang: "fr-BE" },
171
+ "11000221109": { name: "Argentinian Edition", needsBase: true, lang: "es-AR" },
172
+ "11000234105": { name: "Austrian Edition", needsBase: true, lang: "de-AT" },
173
+ "20621000087109": { name: "Canadian Edition (English)", needsBase: true, lang: "en-CA" },
174
+ "20611000087101": { name: "Canadian Edition (French)", needsBase: true, lang: "fr-CA" },
175
+ "11000181102": { name: "Estonian Edition", needsBase: true, lang: "et-EE" },
176
+ "11000229106": { name: "Finnish Edition", needsBase: true, lang: "fi-FI" },
177
+ "11000274103": { name: "German Edition", needsBase: true, lang: "de-DE" },
178
+ "1121000189102": { name: "Indian Edition", needsBase: true, lang: "en-IN" },
179
+ "11000220105": { name: "Irish Edition", needsBase: true, lang: "en-IE" },
180
+ "21000210109": { name: "New Zealand Edition", needsBase: true, lang: "en-NZ" },
181
+ "51000202101": { name: "Norwegian Edition", needsBase: true, lang: "no-NO" },
182
+ "11000267109": { name: "Korean Edition", needsBase: true, lang: "ko-KR" },
183
+ "900000001000122104": { name: "Spanish Edition (Spain)", needsBase: true, lang: "es-ES" },
184
+ "2011000195101": { name: "Swiss Edition", needsBase: true, lang: "de-CH" },
185
+ "999000021000000109": { name: "UK Clinical Edition", needsBase: true, lang: "en-GB" },
186
+ "5631000179106": { name: "Uruguayan Edition", needsBase: true, lang: "es-UY" },
187
+ "21000325107": { name: "Chilean Edition", needsBase: false, lang: "es-CL" },
188
+ "5991000124107": { name: "US Edition + ICD10CM", needsBase: true, lang: "en-US" }
189
+ };
190
+
191
+ const questions = [];
192
+ const inquirer = require('inquirer');
193
+
194
+ // Edition selection (if not provided via options and no URI override)
195
+ if (!options.edition && !options.uri) {
196
+ const editionChoices = Object.entries(editions).map(([id, info]) => ({
197
+ name: info.name,
198
+ value: id
199
+ }));
200
+
201
+ questions.push({
202
+ type: 'list',
203
+ name: 'edition',
204
+ message: 'Select SNOMED CT Edition:',
205
+ choices: editionChoices,
206
+ default: '900000000000207008' // International edition
207
+ });
208
+ }
209
+
210
+ // Version in YYYYMMDD format (if not provided and no URI override)
211
+ if (!options.version && !options.uri) {
212
+ questions.push({
213
+ type: 'input',
214
+ name: 'version',
215
+ message: 'Version (YYYYMMDD format, e.g., 20250801):',
216
+ validate: (input) => {
217
+ if (!input) return 'Version is required';
218
+ if (!/^\d{8}$/.test(input)) return 'Version must be in YYYYMMDD format (8 digits)';
219
+
220
+ // Basic date validation
221
+ const year = parseInt(input.substring(0, 4));
222
+ const month = parseInt(input.substring(4, 6));
223
+ const day = parseInt(input.substring(6, 8));
224
+
225
+ if (year < 1900 || year > 2100) return 'Invalid year';
226
+ if (month < 1 || month > 12) return 'Invalid month';
227
+ if (day < 1 || day > 31) return 'Invalid day';
228
+
229
+ return true;
230
+ }
231
+ });
232
+ }
233
+
234
+ // Get answers for edition and version first
235
+ const primaryAnswers = await inquirer.prompt(questions);
236
+
237
+ // Determine the selected edition and version
238
+ const selectedEdition = options.edition || primaryAnswers.edition;
239
+ const selectedVersion = options.version || primaryAnswers.version;
240
+
241
+ let editionInfo = null;
242
+ let needsBase = false;
243
+ let autoLanguage = 'en-US';
244
+ let autoUri = options.uri;
245
+
246
+ // If we have edition/version (not using URI override), determine settings
247
+ if (selectedEdition && selectedVersion && !options.uri) {
248
+ editionInfo = editions[selectedEdition];
249
+ if (!editionInfo) {
250
+ throw new Error(`Unknown edition: ${selectedEdition}`);
251
+ }
252
+ needsBase = editionInfo.needsBase;
253
+ autoLanguage = editionInfo.lang;
254
+ autoUri = `http://snomed.info/sct/${selectedEdition}/version/${selectedVersion}`;
255
+ } else if (options.uri) {
256
+ // Try to extract edition from URI to determine if base is needed
257
+ const uriMatch = options.uri.match(/sct\/(\d+)\/version/);
258
+ if (uriMatch) {
259
+ const extractedEdition = uriMatch[1];
260
+ editionInfo = editions[extractedEdition];
261
+ if (editionInfo) {
262
+ needsBase = editionInfo.needsBase;
263
+ autoLanguage = editionInfo.lang;
264
+ }
265
+ }
266
+ }
267
+
268
+ // Additional questions based on edition requirements
269
+ const additionalQuestions = [];
270
+
271
+ // Base directory for extensions (only if edition needs base and not already provided)
272
+ if (needsBase && !options.base) {
273
+ additionalQuestions.push({
274
+ type: 'input',
275
+ name: 'base',
276
+ message: 'Base edition directory (required for this edition):',
277
+ validate: (input) => {
278
+ if (!input) return 'Base edition directory is required for this edition';
279
+ if (!fs.existsSync(input)) return 'Directory does not exist';
280
+ return true;
281
+ }
282
+ });
283
+ }
284
+
285
+ // Manual URI input if neither edition/version nor URI was provided
286
+ if (!autoUri && !options.uri) {
287
+ additionalQuestions.push({
288
+ type: 'input',
289
+ name: 'uri',
290
+ message: 'Version URI (e.g., http://snomed.info/sct/900000000000207008/version/20240301):',
291
+ validate: (input) => {
292
+ if (!input) return 'Version URI is required';
293
+ if (!input.includes('snomed.info/sct')) return 'Invalid SNOMED CT URI format';
294
+ return true;
295
+ }
296
+ });
297
+ }
298
+
299
+ const additionalAnswers = additionalQuestions.length > 0 ?
300
+ await inquirer.prompt(additionalQuestions) : {};
301
+
302
+ // Build the final configuration
303
+ const config = {
304
+ ...baseConfig,
305
+ ...options,
306
+ ...primaryAnswers,
307
+ ...additionalAnswers,
308
+ edition: selectedEdition,
309
+ version: selectedVersion,
310
+ language: options.language || autoLanguage, // Allow language override
311
+ uri: options.uri || autoUri || additionalAnswers.uri,
312
+ estimatedDuration: this.getEstimatedDuration()
313
+ };
314
+
315
+ // Validate that we have all required fields
316
+ if (!config.uri) {
317
+ throw new Error('Version URI could not be determined');
318
+ }
319
+
320
+ return config;
321
+ }
322
+
323
+ async runImportWithoutConfigSaving(config) {
324
+ try {
325
+ console.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
326
+
327
+ // Pre-flight checks
328
+ this.logInfo('Running pre-flight checks...');
329
+ const prerequisitesPassed = await this.validatePrerequisites(config);
330
+
331
+ if (!prerequisitesPassed) {
332
+ throw new Error('Pre-flight checks failed');
333
+ }
334
+
335
+ // Execute the import
336
+ await this.executeImport(config);
337
+
338
+ this.logSuccess(`${this.getName()} import completed successfully!`);
339
+
340
+ } catch (error) {
341
+ this.stopProgress();
342
+ this.logError(`${this.getName()} import failed: ${error.message}`);
343
+ if (config.verbose) {
344
+ console.error(error.stack);
345
+ }
346
+ process.exit(1);
347
+ }
348
+ }
349
+
350
+ async executeImport(config) {
351
+ this.logInfo('Starting SNOMED CT data migration...');
352
+
353
+ const importer = new SnomedImporterWithProgress(this, config.verbose);
354
+
355
+ await importer.import(config);
356
+ }
357
+
358
+ async validatePrerequisites(config) {
359
+ const baseValid = await super.validatePrerequisites(config);
360
+
361
+ try {
362
+ this.logInfo('Validating SNOMED CT RF2 directory structure...');
363
+ await this.validateSnomedDirectory(config.source);
364
+ this.logSuccess('SNOMED CT directory structure valid');
365
+ } catch (error) {
366
+ this.logError(`SNOMED CT directory validation failed: ${error.message}`);
367
+ return false;
368
+ }
369
+
370
+ return baseValid;
371
+ }
372
+
373
+ async validateSnomedDirectory(sourceDir) {
374
+ if (!fs.existsSync(sourceDir)) {
375
+ throw new Error(`Source directory not found: ${sourceDir}`);
376
+ }
377
+
378
+ const files = this.discoverRF2Files(sourceDir);
379
+
380
+ if (files.concepts.length === 0) {
381
+ throw new Error('No concept files found');
382
+ }
383
+
384
+ if (files.descriptions.length === 0) {
385
+ throw new Error('No description files found');
386
+ }
387
+
388
+ if (files.relationships.length === 0) {
389
+ throw new Error('No relationship files found');
390
+ }
391
+
392
+ return {
393
+ conceptFiles: files.concepts.length,
394
+ descriptionFiles: files.descriptions.length,
395
+ relationshipFiles: files.relationships.length,
396
+ refsetDirectories: files.refsetDirectories.length
397
+ };
398
+ }
399
+
400
+ discoverRF2Files(dir) {
401
+ const files = {
402
+ concepts: [],
403
+ descriptions: [],
404
+ relationships: [],
405
+ refsetDirectories: []
406
+ };
407
+
408
+ this._scanDirectory(dir, files);
409
+ return files;
410
+ }
411
+
412
+ _scanDirectory(dir, files) {
413
+ if (!fs.existsSync(dir)) return;
414
+
415
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
416
+
417
+ for (const entry of entries) {
418
+ const fullPath = path.join(dir, entry.name);
419
+
420
+ if (entry.isDirectory()) {
421
+ if (entry.name === 'Refset' || entry.name === 'Reference Sets') {
422
+ files.refsetDirectories.push(fullPath);
423
+ } else if (!entry.name.startsWith('.')) {
424
+ this._scanDirectory(fullPath, files);
425
+ }
426
+ } else if (entry.isFile() && entry.name.endsWith('.txt')) {
427
+ this._classifyRF2File(fullPath, files);
428
+ }
429
+ }
430
+ }
431
+
432
+ _classifyRF2File(filePath, files) {
433
+ try {
434
+ const firstLine = this._readFirstLine(filePath);
435
+
436
+ if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tdefinitionStatusId')) {
437
+ files.concepts.push(filePath);
438
+ } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tconceptId\tlanguageCode\ttypeId\tterm\tcaseSignificanceId')) {
439
+ files.descriptions.push(filePath);
440
+ } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tdestinationId\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId') &&
441
+ !filePath.includes('StatedRelationship')) {
442
+ files.relationships.push(filePath);
443
+ }
444
+ } catch (error) {
445
+ // Ignore files we can't read
446
+ }
447
+ }
448
+
449
+ _readFirstLine(filePath) {
450
+ const fd = fs.openSync(filePath, 'r');
451
+ try {
452
+ const buffer = Buffer.alloc(1000);
453
+ const bytesRead = fs.readSync(fd, buffer, 0, 1000, 0);
454
+ const content = buffer.toString('utf8', 0, bytesRead);
455
+ const newlineIndex = content.indexOf('\n');
456
+ return newlineIndex >= 0 ? content.substring(0, newlineIndex) : content;
457
+ } finally {
458
+ fs.closeSync(fd);
459
+ }
460
+ }
461
+
462
+ async handleValidateCommand(options) {
463
+ if (!options.source) {
464
+ const inquirer = require('inquirer');
465
+ const answers = await inquirer.prompt({
466
+ type: 'input',
467
+ name: 'source',
468
+ message: 'Source directory to validate:',
469
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
470
+ });
471
+ options.source = answers.source;
472
+ }
473
+
474
+ this.logInfo(`Validating SNOMED CT directory: ${options.source}`);
475
+
476
+ try {
477
+ const stats = await this.validateSnomedDirectory(options.source);
478
+
479
+ this.logSuccess('Directory validation passed');
480
+ console.log(` Concept files: ${stats.conceptFiles}`);
481
+ console.log(` Description files: ${stats.descriptionFiles}`);
482
+ console.log(` Relationship files: ${stats.relationshipFiles}`);
483
+ console.log(` Refset directories: ${stats.refsetDirectories}`);
484
+
485
+ } catch (error) {
486
+ this.logError(`Validation failed: ${error.message}`);
487
+ }
488
+ }
489
+
490
+ async handleStatusCommand(options) {
491
+ const cachePath = options.dest || './data/snomed.cache';
492
+
493
+ if (!fs.existsSync(cachePath)) {
494
+ this.logError(`Cache file not found: ${cachePath}`);
495
+ return;
496
+ }
497
+
498
+ this.logInfo(`Checking SNOMED CT cache: ${cachePath}`);
499
+
500
+ try {
501
+ // Load and analyze the cache file
502
+ const { SnomedFileReader } = require('./cs-snomed-structures');
503
+ const reader = new SnomedFileReader(cachePath);
504
+ const data = await reader.loadSnomedData();
505
+
506
+ this.logSuccess('Cache file status:');
507
+ console.log(` Cache Version: ${data.cacheVersion}`);
508
+ console.log(` Version URI: ${data.versionUri}`);
509
+ console.log(` Version Date: ${data.versionDate}`);
510
+ console.log(` Edition: ${data.edition}`);
511
+ console.log(` SNOMED Version: ${data.version}`);
512
+
513
+ // Create structure instances to get counts
514
+ const concepts = new SnomedConceptList(data.concept);
515
+ const descriptions = new SnomedDescriptions(data.desc);
516
+ const relationships = new SnomedRelationshipList(data.rel);
517
+
518
+ console.log(` Concepts: ${concepts.count().toLocaleString()}`);
519
+ console.log(` Descriptions: ${descriptions.count().toLocaleString()}`);
520
+ console.log(` Relationships: ${relationships.count().toLocaleString()}`);
521
+ console.log(` Active Roots: ${data.activeRoots.length}`);
522
+ console.log(` Inactive Roots: ${data.inactiveRoots.length}`);
523
+
524
+ const fileStat = fs.statSync(cachePath);
525
+ console.log(` File Size: ${(fileStat.size / (1024 * 1024 * 1024)).toFixed(2)} GB`);
526
+ console.log(` Last Modified: ${fileStat.mtime.toISOString()}`);
527
+
528
+ } catch (error) {
529
+ this.logError(`Status check failed: ${error.message}`);
530
+ }
531
+ }
532
+ }
533
+
534
+ // Enhanced SnomedImporterWithProgress class with timing functionality
535
+
536
+ class SnomedImporterWithProgress {
537
+ constructor(moduleInstance, verbose = true) {
538
+ this.module = moduleInstance;
539
+ this.verbose = verbose;
540
+ this.currentProgressBar = null;
541
+ this.taskStartTimes = new Map();
542
+ }
543
+
544
+ createTaskProgressBar(taskName) {
545
+ if (this.currentProgressBar) {
546
+ this.currentProgressBar.stop();
547
+ }
548
+
549
+ // Record start time for this task
550
+ this.taskStartTimes.set(taskName, Date.now());
551
+
552
+ const progressFormat = `${taskName.padEnd(22)} |{bar}| {percentage}% | {value}/{total} | ETA: {eta}s`;
553
+ this.currentProgressBar = this.module.createProgressBar(progressFormat);
554
+ this.currentProgressBar.taskName = taskName;
555
+
556
+ return this.currentProgressBar;
557
+ }
558
+
559
+ completeTask(taskName, current, total) {
560
+ const startTime = this.taskStartTimes.get(taskName);
561
+ if (startTime && this.currentProgressBar) {
562
+ const elapsedMs = Date.now() - startTime;
563
+ const elapsedSec = (elapsedMs / 1000).toFixed(1);
564
+
565
+ // Stop the progress bar
566
+ this.currentProgressBar.stop();
567
+
568
+ // Build completion message
569
+ let message = `✓ ${taskName} completed: ${current.toLocaleString()}`;
570
+
571
+ if (total && total !== current) {
572
+ message += ` of ${total.toLocaleString()}`;
573
+ // Optional warning if counts don't match
574
+ if (current < total * 0.95) { // More than 5% difference
575
+ message += ` (WARNING: Expected ${total.toLocaleString()})`;
576
+ }
577
+ }
578
+
579
+ // Add timing info
580
+ message += ` items in ${elapsedSec}sec`;
581
+
582
+ // Add rate if meaningful
583
+ if (elapsedMs > 1000 && current > 0) {
584
+ const rate = Math.round(current / (elapsedMs / 1000));
585
+ message += ` (${rate.toLocaleString()} items/sec)`;
586
+ }
587
+
588
+ console.log(message);
589
+
590
+ // Clean up
591
+ this.taskStartTimes.delete(taskName);
592
+ this.currentProgressBar = null;
593
+ }
594
+ }
595
+
596
+ stopCurrentProgress() {
597
+ if (this.currentProgressBar) {
598
+ this.currentProgressBar.stop();
599
+ this.currentProgressBar = null;
600
+ }
601
+ }
602
+
603
+ async import(config) {
604
+ try {
605
+ const importer = new SnomedImporter(config, this);
606
+ await importer.run();
607
+ } finally {
608
+ this.stopCurrentProgress();
609
+ }
610
+ }
611
+ }
612
+
613
+ const IS_A_MAGIC = BigInt('116680003');
614
+ // const RF2_MAGIC_FSN = BigInt('900000000000003001');
615
+ const RF2_MAGIC_RELN_DEFINING = BigInt('900000000000011006');
616
+ const RF2_MAGIC_RELN_STATED = BigInt('900000000000010007');
617
+ const RF2_MAGIC_RELN_INFERRED = BigInt('900000000000006009');
618
+
619
+ // Reference Set field types (matching Pascal)
620
+ const FIELD_TYPE_CONCEPT = 99; // 'c'
621
+ const FIELD_TYPE_INTEGER = 105; // 'i'
622
+ const FIELD_TYPE_STRING = 115; // 's'
623
+
624
+ // Reference Set class to track reference sets during processing
625
+ class RefSet {
626
+ constructor(id) {
627
+ this.id = id;
628
+ this.title = '';
629
+ this.filename = '';
630
+ this.index = 0;
631
+ this.isLangRefset = false;
632
+ this.noStoreIds = false;
633
+ this.langs = 0;
634
+ this.members = [];
635
+ this.membersByRef = 0;
636
+ this.membersByName = 0;
637
+ this.fieldTypes = 0;
638
+ this.fieldNames = 0;
639
+
640
+ // Fast lookup index
641
+ this.memberLookup = new Map(); // componentRef -> member.values
642
+ }
643
+
644
+ addMember(member) {
645
+ this.members.push(member);
646
+ // Build lookup index as we add members
647
+ this.memberLookup.set(member.ref, member.values || 0);
648
+ }
649
+
650
+ // Fast O(1) lookup method
651
+ getMemberValues(componentRef) {
652
+ return this.memberLookup.get(componentRef) || null;
653
+ }
654
+
655
+ // Check if component is a member (O(1))
656
+ hasMember(componentRef) {
657
+ return this.memberLookup.has(componentRef);
658
+ }
659
+ }
660
+
661
+ // Reference Set Member structure
662
+ class RefSetMember {
663
+ constructor() {
664
+ this.id = null; // GUID buffer or null
665
+ this.kind = 0; // 0=concept, 1=description, 2=relationship, 3=other
666
+ this.ref = 0; // Reference to the component
667
+ this.module = 0; // Module concept index
668
+ this.date = 0; // SNOMED date
669
+ this.values = 0; // Index to additional field values
670
+ }
671
+ }
672
+
673
+ class ConceptTracker {
674
+ constructor() {
675
+ this.activeParents = [];
676
+ this.inactiveParents = [];
677
+ this.inbounds = [];
678
+ this.outbounds = [];
679
+ this.descriptions = [];
680
+ }
681
+
682
+ addActiveParent(index) {
683
+ this.activeParents.push(index);
684
+ }
685
+
686
+ addInactiveParent(index) {
687
+ this.inactiveParents.push(index);
688
+ }
689
+
690
+ addInbound(index) {
691
+ this.inbounds.push(index);
692
+ }
693
+
694
+ addOutbound(index) {
695
+ this.outbounds.push(index);
696
+ }
697
+
698
+ addDescription(index) {
699
+ this.descriptions.push(index);
700
+ }
701
+ }
702
+
703
+
704
+ // Main SNOMED CT importer class
705
+ class SnomedImporter {
706
+ static LANGUAGE_STEMMERS = {
707
+ 'en': natural.PorterStemmer, // English
708
+ 'en-US': natural.PorterStemmer, // English (US)
709
+ 'en-GB': natural.PorterStemmer, // English (GB)
710
+ 'fr': natural.PorterStemmerFr, // French
711
+ 'es': natural.PorterStemmerEs, // Spanish
712
+ 'it': natural.PorterStemmerIt, // Italian
713
+ 'pt': natural.PorterStemmerPt, // Portuguese
714
+ 'nl': natural.PorterStemmerNl, // Dutch
715
+ 'no': natural.PorterStemmerNo, // Norwegian
716
+ 'ru': natural.PorterStemmerRu, // Russian
717
+ 'sv': natural.PorterStemmer, // Swedish (fallback to English)
718
+ 'da': natural.PorterStemmer, // Danish (fallback to English)
719
+ 'de': natural.PorterStemmer // German (fallback to English)
720
+ };
721
+
722
+ // Word flags (matching Pascal constants)
723
+ static FLAG_WORD_DEP = 1; // Word appears in active descriptions
724
+ static FLAG_WORD_FSN = 2; // Word appears in FSN (Fully Specified Name)
725
+
726
+ constructor(config, progressReporter = null) {
727
+ this.config = config;
728
+ this.progressReporter = progressReporter;
729
+
730
+ // Initialize data structures
731
+ this.strings = new SnomedStrings();
732
+ this.words = new SnomedWords();
733
+ this.stems = new SnomedStems();
734
+ this.refs = new SnomedReferences();
735
+ this.descriptions = new SnomedDescriptions();
736
+ this.descriptionIndex = new SnomedDescriptionIndex();
737
+ this.concepts = new SnomedConceptList();
738
+ this.relationships = new SnomedRelationshipList();
739
+ this.refsetMembers = new SnomedReferenceSetMembers();
740
+ this.refsetIndex = new SnomedReferenceSetIndex();
741
+
742
+ // Working data
743
+ this.conceptMap = new Map(); // UInt64 -> concept data
744
+ this.conceptList = [];
745
+ this.stringCache = new Map();
746
+ this.relationshipMap = new Map();
747
+ this.conceptTrackers = new Map(); // conceptIndex -> ConceptTracker
748
+ this.refSets = new Map();
749
+ this.refSetTypes = new Map();
750
+ this.processedRefSetCount = 0;
751
+
752
+ this.isAIndex = null;
753
+ this.isTesting = false;
754
+
755
+ // File lists
756
+ this.files = null;
757
+ this.building = true; // Set to true during import
758
+ this.depthProcessedCount = 0; // Track depth processing for progress
759
+ }
760
+
761
+ async run() {
762
+ try {
763
+
764
+ // Discover files
765
+ this.files = this.discoverFiles();
766
+
767
+ // Initialize builders
768
+ this.strings.startBuild();
769
+ this.refs.startBuild();
770
+ this.descriptions.startBuild();
771
+ this.concepts.startBuild();
772
+ this.relationships.startBuild();
773
+ this.refsetIndex.startBuild();
774
+ this.refsetMembers.startBuild();
775
+
776
+ // Step 1: Read concepts
777
+ await this.readConcepts();
778
+
779
+ // Step 2: Sort concepts
780
+ this.sortConcepts();
781
+
782
+ // Step 3: Build concept cache
783
+ this.buildConceptCache();
784
+
785
+ // Step 4: Read descriptions
786
+ await this.readDescriptions();
787
+
788
+ // Step 5: Sort descriptions
789
+ this.sortDescriptions();
790
+
791
+ // Step 6: Build description cache
792
+ this.buildDescriptionCache();
793
+
794
+ // Step 7-9: Process words and stems
795
+ this.processWords();
796
+
797
+ // Step 10: Read relationships
798
+ await this.readRelationships();
799
+
800
+ // Step 11: Link concepts
801
+ this.linkConcepts();
802
+
803
+ // Step 13-15: Reference sets
804
+ await this.processRefsets();
805
+
806
+ // Step 12: Build closure
807
+ this.buildClosure();
808
+
809
+ // Step 16: Set depths
810
+ this.setDepths();
811
+
812
+ // Step 17: Normal forms
813
+ this.buildNormalForms();
814
+
815
+ // Step 18: Save
816
+ await this.saveCache();
817
+
818
+ } catch (error) {
819
+ console.error('DEBUG: Import failed with error:', error);
820
+ throw new Error(`Import failed: ${error.message}`);
821
+ }
822
+ }
823
+
824
+ discoverFiles() {
825
+ const files = {
826
+ concepts: [],
827
+ descriptions: [],
828
+ relationships: [],
829
+ refsetDirectories: []
830
+ };
831
+
832
+ this._scanDirectory(this.config.source, files);
833
+ return files;
834
+ }
835
+
836
+ _scanDirectory(dir, files) {
837
+ if (!fs.existsSync(dir)) {
838
+ return;
839
+ }
840
+
841
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
842
+
843
+ for (const entry of entries) {
844
+ const fullPath = path.join(dir, entry.name);
845
+
846
+ if (entry.isDirectory()) {
847
+ if (entry.name === 'Refset' || entry.name === 'Reference Sets') {
848
+ files.refsetDirectories.push(fullPath);
849
+ } else if (!entry.name.startsWith('.')) {
850
+ this._scanDirectory(fullPath, files);
851
+ }
852
+ } else if (entry.isFile() && entry.name.endsWith('.txt')) {
853
+ this._classifyRF2File(fullPath, files);
854
+ }
855
+ }
856
+ }
857
+
858
+ _classifyRF2File(filePath, files) {
859
+ try {
860
+ const firstLine = this._readFirstLine(filePath);
861
+
862
+ if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tdefinitionStatusId')) {
863
+ files.concepts.push(filePath);
864
+ } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tconceptId\tlanguageCode\ttypeId\tterm\tcaseSignificanceId')) {
865
+ files.descriptions.push(filePath);
866
+ } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tdestinationId\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId') &&
867
+ !filePath.includes('StatedRelationship')) {
868
+ files.relationships.push(filePath);
869
+ }
870
+ } catch (error) {
871
+ console.log(`DEBUG: Error reading file ${filePath}: ${error.message}`);
872
+ }
873
+ }
874
+
875
+ _readFirstLine(filePath) {
876
+ const fd = fs.openSync(filePath, 'r');
877
+ try {
878
+ const buffer = Buffer.alloc(1000);
879
+ const bytesRead = fs.readSync(fd, buffer, 0, 1000, 0);
880
+ const content = buffer.toString('utf8', 0, bytesRead);
881
+ const newlineIndex = content.indexOf('\n');
882
+ return newlineIndex >= 0 ? content.substring(0, newlineIndex) : content;
883
+ } finally {
884
+ fs.closeSync(fd);
885
+ }
886
+ }
887
+
888
+ addString(str) {
889
+ if (!this.stringCache.has(str)) {
890
+ const offset = this.strings.addString(str);
891
+ this.stringCache.set(str, offset);
892
+ return offset;
893
+ }
894
+ return this.stringCache.get(str);
895
+ }
896
+
897
+ async readConcepts() {
898
+ // First, estimate total lines for progress bar
899
+ let totalLines = 0;
900
+ for (const file of this.files.concepts) {
901
+ try {
902
+ const lineCount = await this.countLines(file);
903
+ totalLines += Math.max(0, lineCount - 1); // Subtract 1 for header
904
+ } catch (error) {
905
+ // Use rough estimate if we can't count
906
+ totalLines += 100000;
907
+ }
908
+ }
909
+
910
+ // Create progress bar for this task
911
+ const progressBar = this.progressReporter?.createTaskProgressBar('Reading Concepts');
912
+ progressBar?.start(totalLines, 0);
913
+
914
+ this.conceptList = [];
915
+ let processedLines = 0;
916
+
917
+ for (let i = 0; i < this.files.concepts.length; i++) {
918
+ const file = this.files.concepts[i];
919
+ const rl = readline.createInterface({
920
+ input: fs.createReadStream(file),
921
+ crlfDelay: Infinity
922
+ });
923
+
924
+ let lineCount = 0;
925
+ for await (const line of rl) {
926
+ lineCount++;
927
+ if (lineCount === 1) continue; // Skip header
928
+
929
+ // Parse RF2 concept line: id, effectiveTime, active, moduleId, definitionStatusId
930
+ const parts = line.split('\t');
931
+ if (parts.length >= 5) {
932
+ const concept = {
933
+ id: BigInt(parts[0]),
934
+ effectiveTime: parts[1],
935
+ active: parts[2] === '1',
936
+ moduleId: BigInt(parts[3]),
937
+ definitionStatusId: BigInt(parts[4]),
938
+ index: 0 // Will be set later
939
+ };
940
+
941
+ if (this.conceptMap.has(concept.id)) {
942
+ throw new Error(`Duplicate Concept Id at line ${lineCount}: ${concept.id} - check you are processing the snapshot not the full edition`);
943
+ } else {
944
+ this.conceptList.push(concept);
945
+ this.conceptMap.set(concept.id, concept);
946
+ }
947
+ }
948
+
949
+ this.isTesting = this.conceptMap.has(BigInt(31000003106));
950
+
951
+ processedLines++;
952
+ if (processedLines % 1000 === 0) {
953
+ progressBar?.update(processedLines);
954
+ }
955
+ }
956
+ }
957
+
958
+ // Use completeTask instead of manual update
959
+ if (this.progressReporter) {
960
+ this.progressReporter.completeTask('Reading Concepts', processedLines, totalLines);
961
+ }
962
+ }
963
+
964
+ sortConcepts() {
965
+ this.conceptList.sort((a, b) => {
966
+ if (a.id < b.id) return -1;
967
+ if (a.id > b.id) return 1;
968
+ return 0;
969
+ });
970
+ }
971
+
972
+ buildConceptCache() {
973
+ const progressBar = this.progressReporter?.createTaskProgressBar('Building Concepts');
974
+ progressBar?.start(this.conceptList.length, 0);
975
+
976
+ for (let i = 0; i < this.conceptList.length; i++) {
977
+ const concept = this.conceptList[i];
978
+ const flags = concept.active ? 0 : 1;
979
+ const effectiveTime = this.convertDateToSnomedDate(concept.effectiveTime);
980
+
981
+ concept.index = this.concepts.addConcept(concept.id, effectiveTime, flags);
982
+ concept.stems = []; // Initialize stems array
983
+
984
+ if (i % 1000 === 0) {
985
+ progressBar?.update(i);
986
+ }
987
+ }
988
+
989
+ this.concepts.doneBuild();
990
+
991
+ if (this.progressReporter) {
992
+ this.progressReporter.completeTask('Building Concepts', this.conceptList.length, this.conceptList.length);
993
+ }
994
+ }
995
+
996
+ async readDescriptions() {
997
+ // Estimate total lines
998
+ let totalLines = 0;
999
+ for (const file of this.files.descriptions) {
1000
+ try {
1001
+ const lineCount = await this.countLines(file);
1002
+ totalLines += Math.max(0, lineCount - 1);
1003
+ } catch (error) {
1004
+ totalLines += 100000;
1005
+ }
1006
+ }
1007
+
1008
+ const progressBar = this.progressReporter?.createTaskProgressBar('Reading Descriptions');
1009
+ progressBar?.start(totalLines, 0);
1010
+
1011
+ const descriptionList = [];
1012
+ let processedLines = 0;
1013
+
1014
+ for (const file of this.files.descriptions) {
1015
+ const rl = readline.createInterface({
1016
+ input: fs.createReadStream(file),
1017
+ crlfDelay: Infinity
1018
+ });
1019
+
1020
+ let lineCount = 0;
1021
+ for await (const line of rl) {
1022
+ lineCount++;
1023
+ if (lineCount === 1) continue;
1024
+
1025
+ const parts = line.split('\t');
1026
+ if (parts.length >= 9) {
1027
+ const desc = {
1028
+ id: BigInt(parts[0]),
1029
+ effectiveTime: parts[1],
1030
+ active: parts[2] === '1',
1031
+ moduleId: BigInt(parts[3]),
1032
+ conceptId: BigInt(parts[4]),
1033
+ languageCode: parts[5],
1034
+ typeId: BigInt(parts[6]),
1035
+ term: parts[7],
1036
+ caseSignificanceId: BigInt(parts[8])
1037
+ };
1038
+
1039
+ descriptionList.push(desc);
1040
+ }
1041
+
1042
+ processedLines++;
1043
+ if (processedLines % 1000 === 0) {
1044
+ progressBar?.update(processedLines);
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ this.descriptionList = descriptionList;
1050
+
1051
+ if (this.progressReporter) {
1052
+ this.progressReporter.completeTask('Reading Descriptions', processedLines, totalLines);
1053
+ }
1054
+ }
1055
+
1056
+ sortDescriptions() {
1057
+ // Sort by ID for indexing
1058
+ this.descriptionList.sort((a, b) => {
1059
+ if (a.id < b.id) return -1;
1060
+ if (a.id > b.id) return 1;
1061
+ return 0;
1062
+ });
1063
+ }
1064
+
1065
+ buildDescriptionCache() {
1066
+ const progressBar = this.progressReporter?.createTaskProgressBar('Building Descriptions');
1067
+ progressBar?.start(this.descriptionList.length, 0);
1068
+
1069
+ const indexEntries = [];
1070
+
1071
+ for (let i = 0; i < this.descriptionList.length; i++) {
1072
+ const desc = this.descriptionList[i];
1073
+ const concept = this.conceptMap.get(desc.conceptId);
1074
+
1075
+ if (concept) {
1076
+ const termOffset = this.addString(desc.term);
1077
+ const effectiveTime = this.convertDateToSnomedDate(desc.effectiveTime);
1078
+ const lang = this.mapLanguageCode(desc.languageCode);
1079
+ const kind = this.conceptMap.get(desc.typeId);
1080
+ const module = this.conceptMap.get(desc.moduleId);
1081
+ const caps = this.conceptMap.get(desc.caseSignificanceId);
1082
+
1083
+ const descOffset = this.descriptions.addDescription(
1084
+ termOffset, desc.id, effectiveTime, concept.index,
1085
+ module.index, kind.index, caps.index, desc.active, lang
1086
+ );
1087
+
1088
+ // Track description on concept
1089
+ const conceptTracker = this.getOrCreateConceptTracker(concept.index);
1090
+ conceptTracker.addDescription(descOffset);
1091
+
1092
+ indexEntries.push({ id: desc.id, offset: descOffset });
1093
+ }
1094
+
1095
+ if (i % 1000 === 0) {
1096
+ progressBar?.update(i);
1097
+ }
1098
+ }
1099
+
1100
+ this.descriptions.doneBuild();
1101
+
1102
+ // Build description index
1103
+ this.descriptionIndex.startBuild();
1104
+ indexEntries.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1105
+ for (const entry of indexEntries) {
1106
+ this.descriptionIndex.addDescription(entry.id, entry.offset);
1107
+ }
1108
+ this.descriptionIndex.doneBuild();
1109
+
1110
+ if (this.progressReporter) {
1111
+ this.progressReporter.completeTask('Building Descriptions', this.descriptionList.length, this.descriptionList.length);
1112
+ }
1113
+ }
1114
+
1115
+ // Convert YYYYMMDD format to 16-bit SNOMED date (days since December 30, 1899)
1116
+ convertDateToSnomedDate(dateStr) {
1117
+ if (!dateStr || dateStr.length !== 8) {
1118
+ return 0;
1119
+ }
1120
+
1121
+ const year = parseInt(dateStr.substring(0, 4));
1122
+ const month = parseInt(dateStr.substring(4, 6));
1123
+ const day = parseInt(dateStr.substring(6, 8));
1124
+
1125
+ // Create target date
1126
+ const targetDate = new Date(year, month - 1, day);
1127
+
1128
+ // Pascal TDateTime epoch: December 30, 1899
1129
+ const pascalEpoch = new Date(1899, 11, 30); // Month is 0-based in JS
1130
+
1131
+ // Calculate days difference
1132
+ const daysDiff = Math.floor((targetDate - pascalEpoch) / (1000 * 60 * 60 * 24));
1133
+
1134
+ // Ensure it fits in 16 bits (0-65535) and is positive
1135
+ if (daysDiff < 0 || daysDiff > 65535) {
1136
+ throw new Error(`Date ${dateStr} converts to ${daysDiff}, which is out of 16-bit range`);
1137
+ }
1138
+
1139
+ return daysDiff;
1140
+ }
1141
+
1142
+ mapLanguageCode(code) {
1143
+ // Map language codes to bytes - simplified
1144
+ const langMap = {
1145
+ 'en': 1,
1146
+ 'en-US': 1,
1147
+ 'en-GB': 1,
1148
+ 'fr': 2,
1149
+ 'nl': 3,
1150
+ 'es': 4,
1151
+ 'sv': 5,
1152
+ 'da': 6,
1153
+ 'de': 7,
1154
+ 'it': 8,
1155
+ 'cs': 9
1156
+ };
1157
+ return langMap[code] || 1;
1158
+ }
1159
+
1160
+ processWords() {
1161
+ const progressBar = this.progressReporter?.createTaskProgressBar('Processing Words');
1162
+ progressBar?.start(this.descriptionList.length, 0);
1163
+
1164
+ // Maps to track words and stems
1165
+ const wordMap = new Map(); // word -> {flags, stem, conceptSet}
1166
+ const stemMap = new Map(); // stem -> Set of concept list positions (not concept.index!)
1167
+
1168
+ // Create a map from concept.index to conceptList position for fast lookup
1169
+ const conceptIndexToPosition = new Map();
1170
+ for (let i = 0; i < this.conceptList.length; i++) {
1171
+ conceptIndexToPosition.set(this.conceptList[i].index, i);
1172
+ }
1173
+
1174
+ // Process each description to extract words
1175
+ for (let i = 0; i < this.descriptionList.length; i++) {
1176
+ const desc = this.descriptionList[i];
1177
+ const concept = this.conceptMap.get(desc.conceptId);
1178
+
1179
+ if (concept) {
1180
+ const isActive = desc.active;
1181
+ const isFSN = desc.typeId === BigInt('900000000000003001'); // RF2_MAGIC_FSN
1182
+
1183
+ // Use concept list position instead of concept.index
1184
+ const conceptPosition = conceptIndexToPosition.get(concept.index);
1185
+ if (conceptPosition !== undefined) {
1186
+ this.extractWords(desc.term, desc.languageCode, conceptPosition, isActive, isFSN, wordMap, stemMap);
1187
+ }
1188
+ }
1189
+
1190
+ if (i % 1000 === 0) {
1191
+ progressBar?.update(i);
1192
+ }
1193
+ }
1194
+
1195
+ if (this.progressReporter) {
1196
+ this.progressReporter.completeTask('Processing Words', this.descriptionList.length, this.descriptionList.length);
1197
+ }
1198
+
1199
+ // Build words index
1200
+ const wordsProgressBar = this.progressReporter?.createTaskProgressBar('Building Words Index');
1201
+ wordsProgressBar?.start(wordMap.size, 0);
1202
+
1203
+ this.words.startBuild();
1204
+ let wordIndex = 0;
1205
+
1206
+ for (const [word, wordData] of wordMap) {
1207
+ // Reverse the DEP flag like Pascal does (xor with FLAG_WORD_DEP)
1208
+ const flags = wordData.flags ^ SnomedImporter.FLAG_WORD_DEP;
1209
+ this.words.addWord(this.addString(word), flags);
1210
+
1211
+ if (wordIndex % 1000 === 0) {
1212
+ wordsProgressBar?.update(wordIndex);
1213
+ }
1214
+ wordIndex++;
1215
+ }
1216
+
1217
+ this.words.doneBuild();
1218
+
1219
+ if (this.progressReporter) {
1220
+ this.progressReporter.completeTask('Building Words Index', wordMap.size, wordMap.size);
1221
+ }
1222
+
1223
+ // Build stems index
1224
+ const stemsProgressBar = this.progressReporter?.createTaskProgressBar('Building Stems Index');
1225
+ stemsProgressBar?.start(stemMap.size, 0);
1226
+
1227
+ this.stems.startBuild();
1228
+ let stemIndex = 0;
1229
+
1230
+ for (const [stem, conceptPositionSet] of stemMap) {
1231
+ // Convert concept positions to concept indices for the final index
1232
+ const conceptIndices = Array.from(conceptPositionSet).map(pos => this.conceptList[pos].index);
1233
+ const stemStringIndex = this.addString(stem);
1234
+ const conceptRefsIndex = this.refs.addReferences(conceptIndices);
1235
+
1236
+ this.stems.addStem(stemStringIndex, conceptRefsIndex);
1237
+
1238
+ // Add stem references back to concepts - NOW USING DIRECT ARRAY ACCESS!
1239
+ for (const conceptPosition of conceptPositionSet) {
1240
+ const conceptObj = this.conceptList[conceptPosition]; // O(1) lookup!
1241
+ if (!conceptObj.stems) {
1242
+ conceptObj.stems = [];
1243
+ }
1244
+ conceptObj.stems.push(stemStringIndex);
1245
+ }
1246
+
1247
+ if (stemIndex % 1000 === 0) {
1248
+ stemsProgressBar?.update(stemIndex);
1249
+ }
1250
+ stemIndex++;
1251
+ }
1252
+
1253
+ this.stems.doneBuild();
1254
+
1255
+ if (this.progressReporter) {
1256
+ this.progressReporter.completeTask('Building Stems Index', stemMap.size, stemMap.size);
1257
+ }
1258
+
1259
+ // Mark stems on concepts
1260
+ const markingStemsBar = this.progressReporter?.createTaskProgressBar('Marking Stems');
1261
+ markingStemsBar?.start(this.conceptList.length, 0);
1262
+
1263
+ for (let i = 0; i < this.conceptList.length; i++) {
1264
+ const concept = this.conceptList[i];
1265
+
1266
+ if (concept.stems && concept.stems.length > 0) {
1267
+ // Sort stems and add to concept
1268
+ concept.stems.sort((a, b) => a - b);
1269
+ const stemsRefsIndex = this.refs.addReferences(concept.stems);
1270
+ this.concepts.setStems(concept.index, stemsRefsIndex);
1271
+ }
1272
+
1273
+ if (i % 1000 === 0) {
1274
+ markingStemsBar?.update(i);
1275
+ }
1276
+ }
1277
+
1278
+ if (this.progressReporter) {
1279
+ this.progressReporter.completeTask('Marking Stems', this.conceptList.length, this.conceptList.length);
1280
+ }
1281
+ }
1282
+
1283
+ // Add this new method to extract words from description text:
1284
+ extractWords(text, languageCode, conceptPosition, isActive, isFSN, wordMap, stemMap) {
1285
+ // Get appropriate stemmer for language
1286
+ const stemmer = SnomedImporter.LANGUAGE_STEMMERS[languageCode] || natural.PorterStemmer;
1287
+
1288
+ // Split text on punctuation and whitespace (matching Pascal logic)
1289
+ const separators = /[,\s:.!@#$%^&*(){}[\]|\\;"<>?/~`\-_+=]+/;
1290
+ const words = text.split(separators);
1291
+
1292
+ for (let word of words) {
1293
+ word = word.trim().toLowerCase();
1294
+
1295
+ // Filter words (matching Pascal conditions)
1296
+ if (!word || this.isInteger(word) || word.length <= 2) {
1297
+ continue;
1298
+ }
1299
+
1300
+ // Get or create word entry
1301
+ let wordData = wordMap.get(word);
1302
+ if (!wordData) {
1303
+ const stem = stemmer.stem(word);
1304
+ wordData = {
1305
+ flags: 0,
1306
+ stem: stem,
1307
+ conceptSet: new Set()
1308
+ };
1309
+ wordMap.set(word, wordData);
1310
+ }
1311
+
1312
+ // Update word flags
1313
+ if (isFSN) {
1314
+ wordData.flags |= SnomedImporter.FLAG_WORD_FSN;
1315
+ }
1316
+ if (isActive) {
1317
+ wordData.flags |= SnomedImporter.FLAG_WORD_DEP; // Will be reversed later like Pascal
1318
+ }
1319
+
1320
+ // Add concept position to stem mapping (not concept.index!)
1321
+ let stemConceptSet = stemMap.get(wordData.stem);
1322
+ if (!stemConceptSet) {
1323
+ stemConceptSet = new Set();
1324
+ stemMap.set(wordData.stem, stemConceptSet);
1325
+ }
1326
+ stemConceptSet.add(conceptPosition);
1327
+ }
1328
+ }
1329
+
1330
+ // Helper method to check if string is an integer
1331
+ isInteger(str) {
1332
+ return /^\d+$/.test(str);
1333
+ }
1334
+
1335
+ async readRelationships() {
1336
+ // Estimate total lines
1337
+ let totalLines = 0;
1338
+ for (const file of this.files.relationships) {
1339
+ try {
1340
+ const lineCount = await this.countLines(file);
1341
+ totalLines += Math.max(0, lineCount - 1);
1342
+ } catch (error) {
1343
+ totalLines += 100000;
1344
+ }
1345
+ }
1346
+
1347
+ const progressBar = this.progressReporter?.createTaskProgressBar('Reading Relationships');
1348
+ progressBar?.start(totalLines, 0);
1349
+
1350
+ let processedLines = 0;
1351
+
1352
+ // Find the is-a concept index
1353
+ const isAConcept = this.conceptMap.get(IS_A_MAGIC);
1354
+ if (!isAConcept) {
1355
+ throw new Error('Is-a concept not found (116680003)');
1356
+ }
1357
+ this.isAIndex = isAConcept.index;
1358
+
1359
+ for (const file of this.files.relationships) {
1360
+ const rl = readline.createInterface({
1361
+ input: fs.createReadStream(file),
1362
+ crlfDelay: Infinity
1363
+ });
1364
+
1365
+ let lineCount = 0;
1366
+ for await (const line of rl) {
1367
+ lineCount++;
1368
+ if (lineCount === 1) continue;
1369
+
1370
+ const parts = line.split('\t');
1371
+ if (parts.length >= 10) {
1372
+ const rel = {
1373
+ id: BigInt(parts[0]),
1374
+ effectiveTime: parts[1],
1375
+ active: parts[2] === '1',
1376
+ moduleId: BigInt(parts[3]),
1377
+ sourceId: BigInt(parts[4]),
1378
+ destinationId: BigInt(parts[5]),
1379
+ relationshipGroup: parseInt(parts[6]),
1380
+ typeId: BigInt(parts[7]),
1381
+ characteristicTypeId: BigInt(parts[8]),
1382
+ modifierId: BigInt(parts[9])
1383
+ };
1384
+
1385
+ const source = this.conceptMap.get(rel.sourceId);
1386
+ const destination = this.conceptMap.get(rel.destinationId);
1387
+ const type = this.conceptMap.get(rel.typeId);
1388
+
1389
+ if (source && destination && type) {
1390
+ const effectiveTime = this.convertDateToSnomedDate(rel.effectiveTime);
1391
+
1392
+ // Check if this is a defining relationship
1393
+ const defining = rel.characteristicTypeId === RF2_MAGIC_RELN_DEFINING ||
1394
+ rel.characteristicTypeId === RF2_MAGIC_RELN_STATED ||
1395
+ rel.characteristicTypeId === RF2_MAGIC_RELN_INFERRED;
1396
+
1397
+ const relationshipIndex = this.relationships.addRelationship(
1398
+ rel.id, source.index, destination.index, type.index,
1399
+ 0, 0, 0, effectiveTime, rel.active, defining, rel.relationshipGroup
1400
+ );
1401
+
1402
+ // Track parent/child relationships for is-a relationships
1403
+ if (type.index === this.isAIndex && defining) {
1404
+ const sourceTracker = this.getOrCreateConceptTracker(source.index);
1405
+ if (rel.active) {
1406
+ sourceTracker.addActiveParent(destination.index);
1407
+ } else {
1408
+ sourceTracker.addInactiveParent(destination.index);
1409
+ }
1410
+ }
1411
+
1412
+ // Track inbound/outbound relationships
1413
+ const sourceTracker = this.getOrCreateConceptTracker(source.index);
1414
+ const destTracker = this.getOrCreateConceptTracker(destination.index);
1415
+
1416
+ sourceTracker.addOutbound(relationshipIndex);
1417
+ destTracker.addInbound(relationshipIndex);
1418
+
1419
+ }
1420
+ }
1421
+
1422
+ processedLines++;
1423
+ if (processedLines % 1000 === 0) {
1424
+ progressBar?.update(processedLines);
1425
+ }
1426
+ }
1427
+ }
1428
+
1429
+ this.relationships.doneBuild();
1430
+
1431
+ if (this.progressReporter) {
1432
+ this.progressReporter.completeTask('Reading Relationships', processedLines, totalLines);
1433
+ }
1434
+ }
1435
+
1436
+ getOrCreateConceptTracker(conceptIndex) {
1437
+ if (!this.conceptTrackers.has(conceptIndex)) {
1438
+ this.conceptTrackers.set(conceptIndex, new ConceptTracker());
1439
+ }
1440
+ return this.conceptTrackers.get(conceptIndex);
1441
+ }
1442
+
1443
+ linkConcepts() {
1444
+ const progressBar = this.progressReporter?.createTaskProgressBar('Cross-Link Concepts');
1445
+ progressBar?.start(this.conceptList.length, 0);
1446
+
1447
+ const activeRoots = [];
1448
+ const inactiveRoots = [];
1449
+
1450
+ for (let i = 0; i < this.conceptList.length; i++) {
1451
+ const concept = this.conceptList[i];
1452
+ const tracker = this.conceptTrackers.get(concept.index);
1453
+
1454
+ // Verify concept exists in concept list
1455
+ const foundConcept = this.concepts.findConcept(concept.id);
1456
+ if (!foundConcept.found) {
1457
+ throw new Error(`Import error: concept ${concept.id} not found`);
1458
+ }
1459
+ if (foundConcept.index !== concept.index) {
1460
+ throw new Error(`Import error: concept ${concept.id} index mismatch (${foundConcept.index} vs ${concept.index})`);
1461
+ }
1462
+
1463
+ if (tracker) {
1464
+ // Set parents if concept has any
1465
+ if (tracker.activeParents.length > 0 || tracker.inactiveParents.length > 0) {
1466
+ const activeParentsRef = tracker.activeParents.length > 0 ?
1467
+ this.refs.addReferences(tracker.activeParents) : 0;
1468
+ const inactiveParentsRef = tracker.inactiveParents.length > 0 ?
1469
+ this.refs.addReferences(tracker.inactiveParents) : 0;
1470
+
1471
+ this.concepts.setParents(concept.index, activeParentsRef, inactiveParentsRef);
1472
+ } else {
1473
+ // Concept has no parents - it's a root
1474
+ if (concept.active) {
1475
+ activeRoots.push(concept.id);
1476
+ } else {
1477
+ inactiveRoots.push(concept.id);
1478
+ }
1479
+ }
1480
+
1481
+ // Set descriptions
1482
+ if (tracker.descriptions.length > 0) {
1483
+ const descriptionsRef = this.refs.addReferences(tracker.descriptions);
1484
+ this.concepts.setDescriptions(concept.index, descriptionsRef);
1485
+ }
1486
+
1487
+ // Set inbound relationships (sorted)
1488
+ if (tracker.inbounds.length > 0) {
1489
+ const sortedInbounds = this.sortRelationshipArray(tracker.inbounds);
1490
+ const inboundsRef = this.refs.addReferences(sortedInbounds);
1491
+ this.concepts.setInbounds(concept.index, inboundsRef);
1492
+ }
1493
+
1494
+ // Set outbound relationships (sorted)
1495
+ if (tracker.outbounds.length > 0) {
1496
+ const sortedOutbounds = this.sortRelationshipArray(tracker.outbounds);
1497
+ const outboundsRef = this.refs.addReferences(sortedOutbounds);
1498
+ this.concepts.setOutbounds(concept.index, outboundsRef);
1499
+ }
1500
+ } else {
1501
+ // Concept has no relationships - likely a root
1502
+ if (concept.active) {
1503
+ activeRoots.push(concept.id);
1504
+ } else {
1505
+ inactiveRoots.push(concept.id);
1506
+ }
1507
+ }
1508
+
1509
+ if (i % 1000 === 0) {
1510
+ progressBar?.update(i);
1511
+ }
1512
+ }
1513
+
1514
+ if (activeRoots.length === 0) {
1515
+ throw new Error('No active root concepts found');
1516
+ }
1517
+
1518
+ this.activeRoots = activeRoots;
1519
+ this.inactiveRoots = inactiveRoots;
1520
+
1521
+ if (this.progressReporter) {
1522
+ this.progressReporter.completeTask('Cross-Link Concepts', this.conceptList.length, this.conceptList.length);
1523
+ }
1524
+ }
1525
+
1526
+ // Sort relationship array (simplified version for now)
1527
+ sortRelationshipArray(relationshipArray) {
1528
+ // Create a copy and sort by relationship index
1529
+ const sorted = [...relationshipArray];
1530
+ sorted.sort((a, b) => a - b);
1531
+ return sorted;
1532
+ }
1533
+
1534
+ buildClosure() {
1535
+ const progressBar = this.progressReporter?.createTaskProgressBar('Building Closure');
1536
+ progressBar?.start(this.conceptList.length, 0);
1537
+
1538
+ let totalProcessedClosureCount = 0;
1539
+
1540
+ for (let i = 0; i < this.conceptList.length; i++) {
1541
+ const concept = this.conceptList[i];
1542
+ this.buildConceptClosure(concept.index);
1543
+ totalProcessedClosureCount++;
1544
+
1545
+ if (totalProcessedClosureCount % 1000 === 0) {
1546
+ progressBar?.update(totalProcessedClosureCount);
1547
+ }
1548
+ }
1549
+
1550
+ if (this.progressReporter) {
1551
+ this.progressReporter.completeTask('Building Closure', this.conceptList.length, this.conceptList.length);
1552
+ }
1553
+ }
1554
+
1555
+ // Build closure for a single concept
1556
+ buildConceptClosure(conceptIndex) {
1557
+ const MAGIC_NO_CHILDREN = 0xFFFFFFFF;
1558
+ const MAGIC_IN_PROGRESS = 0xFFFFFFFE; // One less than MAGIC_NO_CHILDREN
1559
+
1560
+ // Check if already processed
1561
+ const existingClosure = this.concepts.getAllDesc(conceptIndex);
1562
+ if (existingClosure === MAGIC_IN_PROGRESS) {
1563
+ throw new Error(`Circular relationship detected at concept ${conceptIndex}`);
1564
+ }
1565
+ if (existingClosure !== 0) {
1566
+ return; // Already processed
1567
+ }
1568
+
1569
+ // Mark as in progress
1570
+ this.concepts.setAllDesc(conceptIndex, MAGIC_IN_PROGRESS);
1571
+
1572
+ // Get children (concepts that have this concept as parent)
1573
+ const children = this.listChildren(conceptIndex);
1574
+
1575
+ if (children.length === 0) {
1576
+ // Leaf concept - no descendants
1577
+ this.concepts.setAllDesc(conceptIndex, MAGIC_NO_CHILDREN);
1578
+ return;
1579
+ }
1580
+
1581
+ // Recursively build closure for all children
1582
+ const allDescendants = new Set();
1583
+
1584
+ for (const childIndex of children) {
1585
+ // Build closure for child first
1586
+ this.buildConceptClosure(childIndex);
1587
+
1588
+ // Add child itself
1589
+ allDescendants.add(childIndex);
1590
+
1591
+ // Add child's descendants
1592
+ const childClosure = this.concepts.getAllDesc(childIndex);
1593
+ if (childClosure !== 0 && childClosure !== MAGIC_NO_CHILDREN) {
1594
+ const childDescendants = this.refs.getReferences(childClosure);
1595
+ if (childDescendants) {
1596
+ for (const descendant of childDescendants) {
1597
+ allDescendants.add(descendant);
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ // Convert to sorted array
1604
+ const descendantsArray = Array.from(allDescendants).sort((a, b) => a - b);
1605
+
1606
+ // Store closure
1607
+ const closureRef = this.refs.addReferences(descendantsArray);
1608
+ this.concepts.setAllDesc(conceptIndex, closureRef);
1609
+ }
1610
+
1611
+ // Get direct children of a concept (concepts that have this as an active parent)
1612
+ getConceptChildren(conceptIndex) {
1613
+ const children = [];
1614
+
1615
+ // Get inbound relationships for this concept
1616
+ const inboundsRef = this.concepts.getInbounds(conceptIndex);
1617
+ if (inboundsRef !== 0) {
1618
+ const inbounds = this.refs.getReferences(inboundsRef);
1619
+ if (inbounds) {
1620
+ for (const relIndex of inbounds) {
1621
+ const rel = this.relationships.getRelationship(relIndex);
1622
+
1623
+ // Check if this is an active is-a relationship where this concept is the target
1624
+ if (rel.relType === this.isAIndex && rel.active && rel.defining) {
1625
+ children.push(rel.source);
1626
+ }
1627
+ }
1628
+ }
1629
+ }
1630
+
1631
+ return children;
1632
+ }
1633
+
1634
+ // Set concept depths starting from roots (matches Pascal SetDepths)
1635
+ setDepths() {
1636
+ const progressBar = this.progressReporter?.createTaskProgressBar('Setting Depths');
1637
+ // We'll process all concepts, not just roots, since recursion touches many concepts
1638
+ progressBar?.start(this.conceptList.length, 0);
1639
+
1640
+ this.depthProcessedCount = 0;
1641
+
1642
+ // Process each active root concept
1643
+ for (const rootId of this.activeRoots) {
1644
+ const foundConcept = this.concepts.findConcept(rootId);
1645
+ if (foundConcept.found) {
1646
+ this.setDepth(foundConcept.index, 0);
1647
+ }
1648
+ }
1649
+
1650
+ if (this.progressReporter) {
1651
+ this.progressReporter.completeTask('Setting Depths', this.depthProcessedCount, this.conceptList.length);
1652
+ }
1653
+ }
1654
+
1655
+ // Recursively set depth for a concept and its children (matches Pascal SetDepth)
1656
+ setDepth(conceptIndex, depth) {
1657
+ const currentDepth = this.concepts.getDepth(conceptIndex);
1658
+
1659
+ // Only update if this is the first time we've reached this concept (depth = 0)
1660
+ // or we've found a shorter path (current depth > new depth)
1661
+ if (currentDepth === 0 || currentDepth > depth) {
1662
+ this.concepts.setDepth(conceptIndex, depth);
1663
+
1664
+ if (depth >= 255) {
1665
+ throw new Error('Concept hierarchy too deep');
1666
+ }
1667
+
1668
+ // Count this concept as processed
1669
+ this.depthProcessedCount++;
1670
+ if (this.depthProcessedCount % 10000 === 0) {
1671
+ // Update progress less frequently during recursion to avoid spam
1672
+ if (this.progressReporter && this.progressReporter.currentProgressBar) {
1673
+ this.progressReporter.currentProgressBar.update(this.depthProcessedCount);
1674
+ }
1675
+ }
1676
+
1677
+ // Increment depth for children
1678
+ const nextDepth = depth + 1;
1679
+
1680
+ // Get children and recursively set their depths
1681
+ const children = this.listChildren(conceptIndex);
1682
+ for (const childIndex of children) {
1683
+ this.setDepth(childIndex, nextDepth);
1684
+ }
1685
+ }
1686
+ }
1687
+
1688
+ // List children of a concept (matches Pascal ListChildren)
1689
+ listChildren(conceptIndex) {
1690
+ const children = [];
1691
+
1692
+ // Get inbound relationships for this concept
1693
+ const inboundsRef = this.concepts.getInbounds(conceptIndex);
1694
+ if (inboundsRef !== 0) {
1695
+ const inbounds = this.refs.getReferences(inboundsRef);
1696
+ if (inbounds) {
1697
+ for (const relIndex of inbounds) {
1698
+ const rel = this.relationships.getRelationship(relIndex);
1699
+
1700
+ // Check if this is an active is-a relationship where this concept is the target
1701
+ if (rel.relType === this.isAIndex && rel.active && rel.defining) {
1702
+ children.push(rel.source);
1703
+ }
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ return children;
1709
+ }
1710
+
1711
+ async processRefsets() {
1712
+ if (this.files.refsetDirectories.length === 0) {
1713
+ console.log('No reference set directories found');
1714
+ this.refsetIndex.doneBuild();
1715
+ this.refsetMembers.doneBuild();
1716
+ return;
1717
+ }
1718
+
1719
+ // First, discover all reference set files
1720
+ const refSetFiles = this.discoverRefSetFiles();
1721
+
1722
+ if (refSetFiles.length === 0) {
1723
+ this.refsetIndex.doneBuild();
1724
+ this.refsetMembers.doneBuild();
1725
+ return;
1726
+ }
1727
+
1728
+ const progressBar = this.progressReporter?.createTaskProgressBar('Processing RefSets');
1729
+ progressBar?.start(refSetFiles.length, 0);
1730
+
1731
+ // Process each reference set file
1732
+ for (let i = 0; i < refSetFiles.length; i++) {
1733
+ const file = refSetFiles[i];
1734
+ await this.loadReferenceSet(file);
1735
+
1736
+ this.processedRefSetCount++;
1737
+ progressBar?.update(this.processedRefSetCount);
1738
+ }
1739
+
1740
+ // Complete strings building so we can READ strings with getEntry()
1741
+ this.strings.doneBuild();
1742
+
1743
+ // Sort and index reference sets (this calls getEntry() so needs doneBuild() first)
1744
+ await this.sortAndIndexRefSets();
1745
+
1746
+ // Reopen strings so we can add reference set titles
1747
+ this.strings.reopen();
1748
+
1749
+ // Add reference sets to index (this needs strings builder to be active)
1750
+ await this.addRefSetsToIndex();
1751
+
1752
+ // Index reference sets by concept
1753
+ await this.indexRefSetsByConcept();
1754
+
1755
+ this.refsetIndex.doneBuild();
1756
+ this.refsetMembers.doneBuild();
1757
+
1758
+ // Complete the task with timing
1759
+ if (this.progressReporter) {
1760
+ this.progressReporter.completeTask('Processing RefSets', refSetFiles.length, refSetFiles.length);
1761
+ }
1762
+ }
1763
+
1764
+ async addRefSetsToIndex() {
1765
+ const refSetsArray = Array.from(this.refSets.values());
1766
+
1767
+ // Add reference sets to index
1768
+ // NOTE: This calls addString() so it must happen AFTER strings.reopen()
1769
+ for (const refSet of refSetsArray) {
1770
+ this.refsetIndex.addReferenceSet(
1771
+ this.addString(refSet.title), // This needs strings builder to be active
1772
+ refSet.filename,
1773
+ refSet.index,
1774
+ refSet.membersByRef,
1775
+ refSet.membersByName,
1776
+ refSet.fieldTypes,
1777
+ refSet.fieldNames,
1778
+ refSet.langs
1779
+ );
1780
+ }
1781
+ }
1782
+
1783
+ // Discover all reference set files
1784
+ discoverRefSetFiles() {
1785
+ const refSetFiles = [];
1786
+
1787
+ for (const refSetDir of this.files.refsetDirectories) {
1788
+ this.scanRefSetDirectory(refSetDir, refSetFiles);
1789
+ }
1790
+
1791
+ return refSetFiles;
1792
+ }
1793
+
1794
+ // Recursively scan reference set directories
1795
+ scanRefSetDirectory(dir, files) {
1796
+ if (!fs.existsSync(dir)) return;
1797
+
1798
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1799
+
1800
+ for (const entry of entries) {
1801
+ const fullPath = path.join(dir, entry.name);
1802
+
1803
+ if (entry.isDirectory()) {
1804
+ if (!entry.name.startsWith('.')) {
1805
+ this.scanRefSetDirectory(fullPath, files);
1806
+ }
1807
+ } else if (entry.isFile() && entry.name.endsWith('.txt')) {
1808
+ // Determine if this is a language reference set
1809
+ const isLangRefset = dir.toLowerCase().includes('language');
1810
+
1811
+ files.push({
1812
+ path: fullPath,
1813
+ isLangRefset: isLangRefset
1814
+ });
1815
+ }
1816
+ }
1817
+ }
1818
+
1819
+ // Load a single reference set file
1820
+ async loadReferenceSet(fileInfo) {
1821
+ const { path: filePath, isLangRefset } = fileInfo;
1822
+
1823
+ try {
1824
+ // Parse filename to extract reference set info
1825
+ const fileName = path.basename(filePath);
1826
+ const parts = fileName.split('_');
1827
+
1828
+ if (parts.length < 3) {
1829
+ console.log(`Skipping file with unexpected name format: ${fileName}`);
1830
+ return;
1831
+ }
1832
+
1833
+ const refSetName = parts[1] || 'Unknown';
1834
+ const displayName = parts[2] || refSetName;
1835
+
1836
+ // Determine field types from filename
1837
+ const fieldTypes = this.parseFieldTypesFromFilename(refSetName);
1838
+
1839
+ const rl = readline.createInterface({
1840
+ input: fs.createReadStream(filePath),
1841
+ crlfDelay: Infinity
1842
+ });
1843
+
1844
+ let lineNumber = 0;
1845
+ let headers = [];
1846
+ let refSet = null;
1847
+ let currentRefSetId = null;
1848
+
1849
+ for await (const line of rl) {
1850
+ lineNumber++;
1851
+
1852
+ if (lineNumber === 1) {
1853
+ // Parse headers
1854
+ headers = line.split('\t').map(h => h.trim());
1855
+
1856
+ // Skip if this looks like a mapping reference set
1857
+ if (line.toLowerCase().includes('map')) {
1858
+ return;
1859
+ }
1860
+ continue;
1861
+ }
1862
+
1863
+ const fields = line.split('\t');
1864
+ if (fields.length < 6) {
1865
+ console.log(`Skipping line ${lineNumber} with insufficient fields: ${fields.length}`);
1866
+ continue; // Minimum fields required
1867
+ }
1868
+
1869
+ // Parse basic fields (standard across all reference sets)
1870
+ const id = fields[0];
1871
+ const effectiveTime = fields[1];
1872
+ const active = fields[2] === '1';
1873
+ const moduleId = fields[3];
1874
+ const refSetId = fields[4];
1875
+ const referencedComponentId = fields[5];
1876
+
1877
+ if (!active) continue; // Only process active members
1878
+
1879
+ // Get or create reference set (only create once per file)
1880
+ if (!refSet || currentRefSetId !== refSetId) {
1881
+ currentRefSetId = refSetId;
1882
+ refSet = this.getOrCreateRefSet(refSetId, displayName, isLangRefset);
1883
+ refSet.filename = this.addString(path.relative(this.config.source, filePath));
1884
+ refSet.fieldTypes = this.getOrCreateFieldTypes(fieldTypes);
1885
+ refSet.fieldNames = this.getOrCreateFieldNames(headers.slice(6), fieldTypes); // Additional fields beyond standard 6
1886
+ }
1887
+
1888
+ // Create reference set member
1889
+ const member = new RefSetMember();
1890
+
1891
+ // Parse GUID
1892
+ try {
1893
+ member.id = this.parseGUID(id);
1894
+ } catch (error) {
1895
+ console.log(`Invalid GUID in ${fileName}: ${id}`);
1896
+ continue;
1897
+ }
1898
+
1899
+ // Find module concept
1900
+ const moduleConcept = this.conceptMap.get(BigInt(moduleId));
1901
+ if (!moduleConcept) {
1902
+ console.log(`Module concept not found: ${moduleId}`);
1903
+ continue;
1904
+ }
1905
+ member.module = moduleConcept.index;
1906
+
1907
+ // Parse effective time
1908
+ member.date = this.convertDateToSnomedDate(effectiveTime);
1909
+
1910
+ // Determine component type and reference
1911
+ const componentId = BigInt(referencedComponentId);
1912
+
1913
+ // Try to find as concept first
1914
+ const concept = this.conceptMap.get(componentId);
1915
+ if (concept) {
1916
+ member.kind = 0; // Concept
1917
+ member.ref = concept.index;
1918
+ } else {
1919
+ // Try to find as description
1920
+ const descResult = this.descriptionIndex.findDescription(componentId);
1921
+ if (descResult.found) {
1922
+ member.kind = 1; // Description
1923
+ member.ref = descResult.index;
1924
+ refSet.noStoreIds = true; // Description reference sets don't store IDs
1925
+
1926
+ // For language reference sets, track languages
1927
+ if (isLangRefset) {
1928
+ const lang = this.getLanguageForDescription(descResult.index);
1929
+ refSet.langs |= (1 << lang);
1930
+ }
1931
+ } else {
1932
+ // Try to find as relationship (simplified - would need relationship ID lookup)
1933
+ member.kind = 3; // Other/unknown
1934
+ member.ref = 0;
1935
+ console.log(`Component not found: ${referencedComponentId}`);
1936
+ continue;
1937
+ }
1938
+ }
1939
+
1940
+ // Process additional fields based on field types
1941
+ if (fieldTypes.length > 0 && fields.length > 6) {
1942
+ const additionalFields = fields.slice(6);
1943
+ member.values = this.processAdditionalFields(additionalFields, fieldTypes);
1944
+ }
1945
+
1946
+ refSet.addMember(member);
1947
+ }
1948
+
1949
+ // Set reference set concept index
1950
+ if (refSet && refSet.index === 0 && currentRefSetId) {
1951
+ const refSetConcept = this.conceptMap.get(BigInt(currentRefSetId));
1952
+ if (refSetConcept) {
1953
+ refSet.index = refSetConcept.index;
1954
+ }
1955
+ }
1956
+
1957
+ } catch (error) {
1958
+ console.error(`Error processing reference set ${filePath}:`, error);
1959
+ }
1960
+ }
1961
+
1962
+ // Parse field types from filename (like "ciRefset" -> ['c', 'i'])
1963
+ parseFieldTypesFromFilename(refSetName) {
1964
+ if (!refSetName.endsWith('Refset') || refSetName === 'Refset') {
1965
+ return [];
1966
+ }
1967
+
1968
+ const typeStr = refSetName.substring(0, refSetName.length - 6); // Remove "Refset"
1969
+ const types = [];
1970
+
1971
+ for (const char of typeStr) {
1972
+ if (char === 'c' || char === 'i' || char === 's') {
1973
+ types.push(char.charCodeAt(0)); // Convert to ASCII code
1974
+ }
1975
+ }
1976
+
1977
+ return types;
1978
+ }
1979
+
1980
+ // Get or create reference set
1981
+ getOrCreateRefSet(refSetId, displayName, isLangRefset) {
1982
+ if (!this.refSets.has(refSetId)) {
1983
+ const refSet = new RefSet(refSetId);
1984
+ refSet.title = displayName;
1985
+ refSet.isLangRefset = isLangRefset;
1986
+ this.refSets.set(refSetId, refSet);
1987
+ }
1988
+ return this.refSets.get(refSetId);
1989
+ }
1990
+
1991
+ // Get or create field types index
1992
+ getOrCreateFieldTypes(fieldTypes) {
1993
+ if (fieldTypes.length === 0) return 0;
1994
+
1995
+ const signature = fieldTypes.join(',');
1996
+ if (!this.refSetTypes.has(signature)) {
1997
+ const typeIndex = this.refs.addReferences(fieldTypes);
1998
+ this.refSetTypes.set(signature, typeIndex);
1999
+ return typeIndex;
2000
+ }
2001
+ return this.refSetTypes.get(signature);
2002
+ }
2003
+
2004
+ // Get or create field names index
2005
+ getOrCreateFieldNames(headers, fieldTypes) {
2006
+ if (headers.length === 0 || fieldTypes.length === 0) return 0;
2007
+
2008
+ const nameIndices = headers.slice(0, fieldTypes.length).map(name => this.addString(name));
2009
+ return this.refs.addReferences(nameIndices);
2010
+ }
2011
+
2012
+ // Process additional fields based on their types
2013
+ processAdditionalFields(fields, fieldTypes) {
2014
+ const values = [];
2015
+
2016
+ for (let i = 0; i < Math.min(fields.length, fieldTypes.length); i++) {
2017
+ const field = fields[i];
2018
+ const fieldType = fieldTypes[i];
2019
+
2020
+ let value, type;
2021
+
2022
+ switch (fieldType) {
2023
+ case FIELD_TYPE_CONCEPT: { // 'c'
2024
+ const conceptId = field ? BigInt(field) : BigInt(0);
2025
+ const concept = this.conceptMap.get(conceptId);
2026
+ if (concept) {
2027
+ value = concept.index;
2028
+ type = 1; // Concept
2029
+ } else {
2030
+ // Try description
2031
+ const descResult = this.descriptionIndex.findDescription(conceptId);
2032
+ if (descResult.found) {
2033
+ value = descResult.index;
2034
+ type = 2; // Description
2035
+ } else {
2036
+ console.log(`Referenced component not found: ${field}`);
2037
+ value = 0;
2038
+ type = 1;
2039
+ }
2040
+ }
2041
+ break;
2042
+ }
2043
+ case FIELD_TYPE_INTEGER: // 'i'
2044
+ value = parseInt(field) || 0;
2045
+ type = 4; // Integer
2046
+ break;
2047
+
2048
+ case FIELD_TYPE_STRING: // 's'
2049
+ value = this.addString(field || '');
2050
+ type = 5; // String
2051
+ break;
2052
+
2053
+ default:
2054
+ value = 0;
2055
+ type = 1;
2056
+ }
2057
+
2058
+ values.push(value, type);
2059
+ }
2060
+
2061
+ return values.length > 0 ? this.refs.addReferences(values) : 0;
2062
+ }
2063
+
2064
+ // Parse GUID string to 16-byte buffer
2065
+ parseGUID(guidString) {
2066
+ // Remove hyphens and braces
2067
+ const cleanGuid = guidString.replace(/[-{}]/g, '');
2068
+ if (cleanGuid.length !== 32) {
2069
+ throw new Error('Invalid GUID format');
2070
+ }
2071
+ return Buffer.from(cleanGuid, 'hex');
2072
+ }
2073
+
2074
+ // Get language for description (placeholder - would need actual implementation)
2075
+ getLanguageForDescription(descIndex) {
2076
+ // This would need to look up the actual description language
2077
+ // For now, return default language
2078
+ var d = this.descriptions.getDescription(descIndex);
2079
+ return d.lang;
2080
+ }
2081
+
2082
+ // Sort and index reference sets
2083
+ async sortAndIndexRefSets() {
2084
+ const refSetsArray = Array.from(this.refSets.values());
2085
+
2086
+ // Sort reference sets by concept index
2087
+ refSetsArray.sort((a, b) => a.index - b.index);
2088
+
2089
+ for (let i = 0; i < refSetsArray.length; i++) {
2090
+ const refSet = refSetsArray[i];
2091
+
2092
+ // Sort members by name (requires looking up descriptions via getEntry())
2093
+ const membersByName = [...refSet.members];
2094
+ this.sortMembersByName(membersByName);
2095
+ refSet.membersByName = this.refsetMembers.addMembers(false, membersByName);
2096
+
2097
+ // Sort members by reference
2098
+ const membersByRef = [...refSet.members];
2099
+ membersByRef.sort((a, b) => a.ref - b.ref);
2100
+ refSet.membersByRef = this.refsetMembers.addMembers(!refSet.noStoreIds, membersByRef);
2101
+ }
2102
+ }
2103
+
2104
+ // Sort reference set members by name (description text)
2105
+ sortMembersByName(members) {
2106
+ members.sort((a, b) => {
2107
+ const nameA = this.getMemberDisplayName(a);
2108
+ const nameB = this.getMemberDisplayName(b);
2109
+ return nameA.localeCompare(nameB);
2110
+ });
2111
+ }
2112
+
2113
+ // Get display name for a reference set member
2114
+ getMemberDisplayName(member) {
2115
+ try {
2116
+ if (member.kind === 1) {
2117
+ // Description - get the term directly
2118
+ const desc = this.descriptions.getDescription(member.ref);
2119
+ return this.strings.getEntry(desc.iDesc);
2120
+ } else if (member.kind === 0) {
2121
+ // Concept - find FSN description
2122
+ const descriptionsRef = this.concepts.getDescriptions(member.ref);
2123
+ if (descriptionsRef !== 0) {
2124
+ const descriptions = this.refs.getReferences(descriptionsRef);
2125
+
2126
+ // Look for FSN first
2127
+ for (const descRef of descriptions) {
2128
+ const desc = this.descriptions.getDescription(descRef);
2129
+ if (desc.active && desc.kind === this.fsnIndex) {
2130
+ return this.strings.getEntry(desc.iDesc);
2131
+ }
2132
+ }
2133
+
2134
+ // Fall back to any active description
2135
+ for (const descRef of descriptions) {
2136
+ const desc = this.descriptions.getDescription(descRef);
2137
+ if (desc.active) {
2138
+ return this.strings.getEntry(desc.iDesc);
2139
+ }
2140
+ }
2141
+ }
2142
+ return `Concept ${member.ref}`;
2143
+ } else {
2144
+ return `Component ${member.ref}`;
2145
+ }
2146
+ } catch (error) {
2147
+ return `Unknown ${member.ref}`;
2148
+ }
2149
+ }
2150
+
2151
+ // Index reference sets by concept for quick lookup
2152
+ async indexRefSetsByConcept() {
2153
+ const totalItems = this.conceptList.length + this.descriptions.count();
2154
+ const progressBar = this.progressReporter?.createTaskProgressBar('Indexing RefSets');
2155
+ progressBar?.start(totalItems, 0);
2156
+
2157
+ let processed = 0;
2158
+
2159
+ // Pre-build array of reference sets for faster iteration
2160
+ const refSetsArray = Array.from(this.refSets.values());
2161
+
2162
+ // Index concepts - optimized version
2163
+ for (const concept of this.conceptList) {
2164
+ const refSetRefs = [];
2165
+ const refSetValues = [];
2166
+
2167
+ // Check each reference set for this concept (now O(1) per refset!)
2168
+ for (const refSet of refSetsArray) {
2169
+ if (refSet.hasMember(concept.index)) {
2170
+ refSetRefs.push(refSet.index);
2171
+ refSetValues.push(refSet.getMemberValues(concept.index));
2172
+ }
2173
+ }
2174
+
2175
+ if (refSetRefs.length > 0) {
2176
+ const refsIndex = this.refs.addReferences(refSetRefs);
2177
+ this.concepts.setRefsets(concept.index, refsIndex);
2178
+ }
2179
+
2180
+ processed++;
2181
+ if (processed % 1000 === 0) {
2182
+ progressBar?.update(processed);
2183
+ }
2184
+ }
2185
+
2186
+ // Index descriptions - optimized version
2187
+ for (let i = 0; i < this.descriptions.count(); i++) {
2188
+ const descIndex = i * 40; // DESC_SIZE = 40
2189
+ const refSetRefs = [];
2190
+ const refSetValues = [];
2191
+
2192
+ // Check each reference set for this description (now O(1) per refset!)
2193
+ for (const refSet of refSetsArray) {
2194
+ if (refSet.hasMember(descIndex)) {
2195
+ refSetRefs.push(refSet.index);
2196
+ refSetValues.push(refSet.getMemberValues(descIndex));
2197
+ }
2198
+ }
2199
+
2200
+ if (refSetRefs.length > 0) {
2201
+ const refsIndex = this.refs.addReferences(refSetRefs);
2202
+ const valuesIndex = this.refs.addReferences(refSetValues);
2203
+ this.descriptions.setRefsets(descIndex, refsIndex, valuesIndex);
2204
+ }
2205
+
2206
+ processed++;
2207
+ if (processed % 1000 === 0) {
2208
+ progressBar?.update(processed);
2209
+ }
2210
+ }
2211
+
2212
+ if (this.progressReporter) {
2213
+ this.progressReporter.completeTask('Indexing RefSets', processed, totalItems);
2214
+ }
2215
+ }
2216
+
2217
+ // Find if a component is a member of a reference set
2218
+ findMemberInRefSet(refSet, componentRef) {
2219
+ return refSet.getMemberValues(componentRef);
2220
+ }
2221
+
2222
+ buildNormalForms() {
2223
+ const progressBar = this.progressReporter?.createTaskProgressBar('Building Normal Forms');
2224
+ progressBar?.start(this.conceptList.length, 0);
2225
+
2226
+ // First complete strings building so we can read from strings
2227
+ this.strings.doneBuild();
2228
+
2229
+ // Create the expression services - we need all structures to be ready
2230
+ const snomedStructures = {
2231
+ strings: this.strings,
2232
+ words: this.words,
2233
+ stems: this.stems,
2234
+ refs: this.refs,
2235
+ descriptions: this.descriptions,
2236
+ descriptionIndex: this.descriptionIndex,
2237
+ concepts: this.concepts,
2238
+ relationships: this.relationships,
2239
+ refSetMembers: this.refsetMembers,
2240
+ refSetIndex: this.refsetIndex
2241
+ };
2242
+
2243
+ const services = new SnomedExpressionServices(
2244
+ snomedStructures,
2245
+ this.isAIndex
2246
+ );
2247
+
2248
+ // Set building flag to true so services will generate normal forms dynamically
2249
+ services.building = true;
2250
+
2251
+ // Now reopen strings so we can add normal form strings
2252
+ this.strings.reopen();
2253
+
2254
+ let processedCount = 0;
2255
+ let normalFormsAdded = 0;
2256
+
2257
+ for (const concept of this.conceptList) {
2258
+ try {
2259
+ // Create expression with just this concept
2260
+ const { SnomedExpression, SnomedConcept } = require('../sct/expressions');
2261
+ const exp = new SnomedExpression();
2262
+ const snomedConcept = new SnomedConcept(concept.index);
2263
+ snomedConcept.code = concept.id.toString();
2264
+ exp.concepts.push(snomedConcept);
2265
+
2266
+ // Normalize the expression
2267
+ const normalizedExp = services.normaliseExpression(exp);
2268
+
2269
+ // Render with minimal formatting
2270
+ const { SnomedServicesRenderOption } = require('../sct/expressions');
2271
+ const rendered = services.renderExpression(normalizedExp, SnomedServicesRenderOption.Minimal);
2272
+
2273
+ // If the rendered form is different from just the concept ID, store it
2274
+ const conceptIdStr = concept.id.toString();
2275
+ if (rendered !== conceptIdStr) {
2276
+ const normalFormStringIndex = this.addString(rendered);
2277
+ this.concepts.setNormalForm(concept.index, normalFormStringIndex);
2278
+ normalFormsAdded++;
2279
+ }
2280
+ // If rendered === conceptIdStr, normal form remains 0 (default)
2281
+
2282
+ } catch (error) {
2283
+ // Log the error but continue processing other concepts
2284
+ if (this.config.verbose) {
2285
+ console.warn(`Warning: Could not build normal form for concept ${concept.id}: ${error.message}`);
2286
+ }
2287
+ }
2288
+
2289
+ processedCount++;
2290
+ if (processedCount % 1000 === 0) {
2291
+ progressBar?.update(processedCount);
2292
+ }
2293
+ }
2294
+
2295
+ if (this.progressReporter) {
2296
+ this.progressReporter.completeTask('Building Normal Forms', processedCount, this.conceptList.length);
2297
+ }
2298
+
2299
+ if (this.config.verbose) {
2300
+ console.log(`Normal forms: ${normalFormsAdded} concepts have non-trivial normal forms`);
2301
+ }
2302
+ }
2303
+
2304
+ async saveCache() {
2305
+ const progressBar = this.progressReporter?.createTaskProgressBar('Saving Cache');
2306
+ progressBar?.start(100, 0);
2307
+
2308
+ this.refs.doneBuild();
2309
+ this.strings.doneBuild();
2310
+
2311
+ progressBar?.update(25);
2312
+
2313
+ // Write the binary cache file using our writer
2314
+ const writer = new SnomedCacheWriter(this.config.dest);
2315
+
2316
+ progressBar?.update(50);
2317
+
2318
+ await writer.writeCache({
2319
+ version: '17', // Current version
2320
+ versionUri: this.isTesting ? this.config.uri.replace("/sct/", "/xsct/") : this.config.uri,
2321
+ versionDate: this.extractDateFromUri(this.config.uri),
2322
+
2323
+ strings: this.strings.master,
2324
+ refs: this.refs.master,
2325
+ desc: this.descriptions.master,
2326
+ words: this.words.master,
2327
+ stems: this.stems.master,
2328
+ concept: this.concepts.master,
2329
+ rel: this.relationships.master,
2330
+ refSetIndex: this.refsetIndex.master,
2331
+ refSetMembers: this.refsetMembers.master,
2332
+ descRef: this.descriptionIndex.master,
2333
+
2334
+ isAIndex: this.isAIndex, // Simplified
2335
+ inactiveRoots: this.inactiveRoots || [],
2336
+ activeRoots: this.activeRoots || [],
2337
+ defaultLanguage: 1
2338
+ });
2339
+
2340
+ if (this.progressReporter) {
2341
+ this.progressReporter.completeTask('Saving Cache', 100, 100);
2342
+ }
2343
+ }
2344
+
2345
+ extractDateFromUri(uri) {
2346
+ // Extract date from URI like http://snomed.info/sct/900000000000207008/version/20240301
2347
+ const match = uri.match(/version\/(\d{8})/);
2348
+ return match ? match[1] : new Date().toISOString().slice(0, 10).replace(/-/g, '');
2349
+ }
2350
+
2351
+ async countLines(filePath) {
2352
+ return new Promise((resolve, reject) => {
2353
+ let lineCount = 0;
2354
+ const rl = readline.createInterface({
2355
+ input: fs.createReadStream(filePath),
2356
+ crlfDelay: Infinity
2357
+ });
2358
+
2359
+ rl.on('line', () => lineCount++);
2360
+ rl.on('close', () => resolve(lineCount));
2361
+ rl.on('error', reject);
2362
+ });
2363
+ }
2364
+ }
2365
+
2366
+ // Cache file writer that matches Pascal TWriter format
2367
+ class SnomedCacheWriter {
2368
+ constructor(filePath) {
2369
+ this.filePath = filePath;
2370
+ }
2371
+
2372
+ async writeCache(data) {
2373
+ const buffers = [];
2374
+
2375
+ // Write version string
2376
+ buffers.push(this.writeString(data.version));
2377
+
2378
+ // Write version URI and date
2379
+ buffers.push(this.writeString(data.versionUri));
2380
+ buffers.push(this.writeString(data.versionDate));
2381
+
2382
+ // Write byte arrays
2383
+ buffers.push(this.writeBytes(data.strings));
2384
+ buffers.push(this.writeBytes(data.refs));
2385
+ buffers.push(this.writeBytes(data.desc));
2386
+ buffers.push(this.writeBytes(data.words));
2387
+ buffers.push(this.writeBytes(data.stems));
2388
+ buffers.push(this.writeBytes(data.concept));
2389
+ buffers.push(this.writeBytes(data.rel));
2390
+ buffers.push(this.writeBytes(data.refSetIndex));
2391
+ buffers.push(this.writeBytes(data.refSetMembers));
2392
+ buffers.push(this.writeBytes(data.descRef));
2393
+
2394
+ // Write integers and arrays
2395
+ buffers.push(this.writeInteger(data.isAIndex));
2396
+ buffers.push(this.writeInteger(data.inactiveRoots.length));
2397
+ for (const root of data.inactiveRoots) {
2398
+ buffers.push(this.writeUInt64(root));
2399
+ }
2400
+ buffers.push(this.writeInteger(data.activeRoots.length));
2401
+ for (const root of data.activeRoots) {
2402
+ buffers.push(this.writeUInt64(root));
2403
+ }
2404
+ buffers.push(this.writeInteger(data.defaultLanguage));
2405
+
2406
+ // Write to file
2407
+ const finalBuffer = Buffer.concat(buffers);
2408
+ await fs.promises.writeFile(this.filePath, finalBuffer);
2409
+ }
2410
+
2411
+ writeString(str) {
2412
+ const utf8Bytes = Buffer.from(str, 'utf8');
2413
+ const length = utf8Bytes.length;
2414
+
2415
+ if (length <= 255) {
2416
+ // Short string: type 6 (vaString) + 1-byte length + string bytes
2417
+ const buffer = Buffer.allocUnsafe(1 + 1 + length);
2418
+ buffer.writeUInt8(6, 0); // vaString type
2419
+ buffer.writeUInt8(length, 1); // 1-byte length
2420
+ utf8Bytes.copy(buffer, 2);
2421
+ return buffer;
2422
+ } else {
2423
+ // Long string: type 12 (vaLString) + 4-byte length + string bytes
2424
+ const buffer = Buffer.allocUnsafe(1 + 4 + length);
2425
+ buffer.writeUInt8(6, 0); // vaLString type
2426
+ buffer.writeUInt32LE(length, 1); // 4-byte length
2427
+ utf8Bytes.copy(buffer, 5);
2428
+ return buffer;
2429
+ }
2430
+ }
2431
+
2432
+ writeInteger(value) {
2433
+ // Type 4 = 4-byte integer
2434
+ const buffer = Buffer.allocUnsafe(5);
2435
+ buffer.writeUInt8(4, 0); // Type byte
2436
+ buffer.writeInt32LE(value, 1);
2437
+ return buffer;
2438
+ }
2439
+
2440
+ writeBytes(byteArray) {
2441
+ const lengthBuffer = this.writeInteger(byteArray.length);
2442
+ return Buffer.concat([lengthBuffer, byteArray]);
2443
+ }
2444
+
2445
+ // 19
2446
+ writeUInt64(value) {
2447
+ const buffer = Buffer.allocUnsafe(8);
2448
+ buffer.writeBigUInt64LE(BigInt(value), 0);
2449
+ return buffer;
2450
+ }
2451
+ }
2452
+
2453
+ module.exports = {
2454
+ SnomedModule,
2455
+ SnomedImporter,
2456
+ SnomedCacheWriter
2457
+ };