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,1081 @@
1
+ const { BaseTerminologyModule } = require('./tx-import-base');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+ const chalk = require('chalk');
6
+
7
+ class LoincSubsetModule extends BaseTerminologyModule {
8
+ constructor() {
9
+ super();
10
+ }
11
+
12
+ getName() {
13
+ return 'loinc-subset';
14
+ }
15
+
16
+ getDescription() {
17
+ return 'Create a subset of LOINC data for testing purposes';
18
+ }
19
+
20
+ getSupportedFormats() {
21
+ return ['directory', 'txt'];
22
+ }
23
+
24
+ getDefaultConfig() {
25
+ return {
26
+ verbose: true,
27
+ overwrite: false,
28
+ dest: './loinc-subset',
29
+ expandPartLinks: true
30
+ };
31
+ }
32
+
33
+ getEstimatedDuration() {
34
+ return '5-15 minutes (depending on subset size)';
35
+ }
36
+
37
+ registerCommands(terminologyCommand, globalOptions) {
38
+ // Subset command
39
+ terminologyCommand
40
+ .command('subset')
41
+ .description('Create a LOINC subset from a list of codes')
42
+ .option('-s, --source <directory>', 'Source LOINC directory')
43
+ .option('-d, --dest <directory>', 'Destination directory for subset')
44
+ .option('-c, --codes <file>', 'Text file with LOINC codes (one per line)')
45
+ .option('-y, --yes', 'Skip confirmations')
46
+ .action(async (options) => {
47
+ await this.handleSubsetCommand({...globalOptions, ...options});
48
+ });
49
+
50
+ // Validate command
51
+ terminologyCommand
52
+ .command('validate')
53
+ .description('Validate subset inputs')
54
+ .option('-s, --source <directory>', 'Source LOINC directory to validate')
55
+ .option('-c, --codes <file>', 'Codes file to validate')
56
+ .action(async (options) => {
57
+ await this.handleValidateCommand({...globalOptions, ...options});
58
+ });
59
+ }
60
+
61
+ async handleSubsetCommand(options) {
62
+ try {
63
+ // Gather configuration
64
+ const config = await this.gatherSubsetConfig(options);
65
+
66
+ // Add estimated duration to config for confirmation display
67
+ config.estimatedDuration = this.getEstimatedDuration();
68
+
69
+ // Show confirmation unless --yes is specified
70
+ if (!options.yes) {
71
+ const confirmed = await this.confirmSubset(config);
72
+ if (!confirmed) {
73
+ this.logInfo('Subset operation cancelled');
74
+ return;
75
+ }
76
+ }
77
+
78
+ // Save configuration immediately after confirmation
79
+ this.rememberSuccessfulConfig(config);
80
+
81
+ // Run the subset operation
82
+ await this.runSubset(config);
83
+ } catch (error) {
84
+ this.logError(`Subset operation failed: ${error.message}`);
85
+ if (options.verbose) {
86
+ console.error(error.stack);
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ async gatherSubsetConfig(options) {
93
+ const terminology = this.getName();
94
+
95
+ // Get intelligent defaults based on previous usage
96
+ const smartDefaults = this.configManager.generateDefaults(terminology);
97
+ const recentSources = this.configManager.getRecentSources(terminology, 3);
98
+
99
+ const questions = [];
100
+
101
+ // Source directory
102
+ if (!options.source) {
103
+ const sourceQuestion = {
104
+ type: 'input',
105
+ name: 'source',
106
+ message: 'Source LOINC directory:',
107
+ validate: (input) => {
108
+ if (!input) return 'Source directory is required';
109
+ if (!fs.existsSync(input)) return 'Source directory does not exist';
110
+ return true;
111
+ },
112
+ filter: (input) => path.resolve(input)
113
+ };
114
+
115
+ // Add default if we have a previous source
116
+ if (smartDefaults.source) {
117
+ sourceQuestion.default = smartDefaults.source;
118
+ }
119
+
120
+ // If we have recent sources, offer them as choices
121
+ if (recentSources.length > 0) {
122
+ sourceQuestion.type = 'list';
123
+ sourceQuestion.choices = [
124
+ ...recentSources.map(src => ({
125
+ name: `${src} ${src === smartDefaults.source ? '(last used)' : ''}`.trim(),
126
+ value: src
127
+ })),
128
+ { name: 'Enter new path...', value: 'NEW_PATH' }
129
+ ];
130
+ sourceQuestion.message = 'Select source LOINC directory:';
131
+ }
132
+
133
+ questions.push(sourceQuestion);
134
+
135
+ // Follow up question for new path - only add if we're using the list approach
136
+ if (recentSources.length > 0) {
137
+ questions.push({
138
+ type: 'input',
139
+ name: 'source',
140
+ message: 'Enter new source path:',
141
+ when: (answers) => answers.source === 'NEW_PATH',
142
+ validate: (input) => {
143
+ if (!input) return 'Source directory is required';
144
+ if (!fs.existsSync(input)) return 'Source directory does not exist';
145
+ return true;
146
+ },
147
+ filter: (input) => path.resolve(input)
148
+ });
149
+ }
150
+ }
151
+
152
+ // Destination directory
153
+ if (!options.dest) {
154
+ questions.push({
155
+ type: 'input',
156
+ name: 'dest',
157
+ message: 'Destination directory:',
158
+ default: smartDefaults.dest || './loinc-subset',
159
+ validate: (input) => {
160
+ if (!input) return 'Destination directory is required';
161
+ return true;
162
+ },
163
+ filter: (input) => path.resolve(input)
164
+ });
165
+ }
166
+
167
+ // Codes file
168
+ if (!options.codes) {
169
+ questions.push({
170
+ type: 'input',
171
+ name: 'codes',
172
+ message: 'Codes file (one code per line):',
173
+ default: smartDefaults.codes,
174
+ validate: (input) => {
175
+ if (!input) return 'Codes file is required';
176
+ if (!fs.existsSync(input)) return 'Codes file does not exist';
177
+ return true;
178
+ },
179
+ filter: (input) => path.resolve(input)
180
+ });
181
+ }
182
+
183
+ // Overwrite confirmation
184
+ questions.push({
185
+ type: 'confirm',
186
+ name: 'overwrite',
187
+ message: 'Overwrite destination directory if it exists?',
188
+ default: smartDefaults.overwrite !== undefined ? smartDefaults.overwrite : false,
189
+ when: (answers) => {
190
+ const destPath = options.dest || answers.dest;
191
+ return fs.existsSync(destPath);
192
+ }
193
+ });
194
+
195
+ questions.push({
196
+ type: 'confirm',
197
+ name: 'expandPartLinks',
198
+ message: 'Expand codes based on PartLink relationships?',
199
+ default: smartDefaults.expandPartLinks !== undefined ? smartDefaults.expandPartLinks : true
200
+ });
201
+
202
+ questions.push({
203
+ type: 'confirm',
204
+ name: 'verbose',
205
+ message: 'Show verbose output?',
206
+ default: smartDefaults.verbose !== undefined ? smartDefaults.verbose : true
207
+ });
208
+
209
+ const answers = await require('inquirer').prompt(questions);
210
+
211
+ const finalConfig = {
212
+ ...this.getDefaultConfig(),
213
+ ...smartDefaults,
214
+ ...options,
215
+ ...answers
216
+ };
217
+
218
+ return finalConfig;
219
+ }
220
+
221
+ async confirmSubset(config) {
222
+ console.log(chalk.cyan(`\n📋 LOINC Subset Configuration:`));
223
+ console.log(` Source: ${chalk.white(config.source)}`);
224
+ console.log(` Destination: ${chalk.white(config.dest)}`);
225
+ console.log(` Codes File: ${chalk.white(config.codes)}`);
226
+ console.log(` Expand PartLinks: ${chalk.white(config.expandPartLinks ? 'Yes' : 'No')}`);
227
+ console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
228
+
229
+ if (config.estimatedDuration) {
230
+ console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
231
+ }
232
+
233
+ const { confirmed } = await require('inquirer').prompt({
234
+ type: 'confirm',
235
+ name: 'confirmed',
236
+ message: 'Proceed with subset creation?',
237
+ default: true
238
+ });
239
+
240
+ return confirmed;
241
+ }
242
+
243
+ async runSubset(config) {
244
+ try {
245
+ console.log(chalk.blue.bold(`🔬 Starting LOINC Subset Creation...\n`));
246
+
247
+ if (config.verbose) {
248
+ console.log('Debug - Final config values:');
249
+ console.log(` Source: ${config.source}`);
250
+ console.log(` Dest: ${config.dest}`);
251
+ console.log(` Codes: ${config.codes}`);
252
+ console.log('');
253
+ }
254
+
255
+ // Pre-flight checks
256
+ this.logInfo('Running pre-flight checks...');
257
+ const prerequisitesPassed = await this.validateSubsetPrerequisites(config);
258
+
259
+ if (!prerequisitesPassed) {
260
+ throw new Error('Pre-flight checks failed');
261
+ }
262
+
263
+ // Execute the subset creation
264
+ await this.executeSubset(config);
265
+
266
+ this.logSuccess('LOINC subset created successfully!');
267
+
268
+ } catch (error) {
269
+ this.stopProgress();
270
+ this.logError(`LOINC subset creation failed: ${error.message}`);
271
+ if (config.verbose) {
272
+ console.error(error.stack);
273
+ }
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ async handleValidateCommand(options) {
279
+ if (!options.source || !options.codes) {
280
+ const answers = await require('inquirer').prompt([
281
+ {
282
+ type: 'input',
283
+ name: 'source',
284
+ message: 'Source LOINC directory:',
285
+ when: !options.source,
286
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
287
+ },
288
+ {
289
+ type: 'input',
290
+ name: 'codes',
291
+ message: 'Codes file:',
292
+ when: !options.codes,
293
+ validate: (input) => input && fs.existsSync(input) ? true : 'File does not exist'
294
+ }
295
+ ]);
296
+ Object.assign(options, answers);
297
+ }
298
+
299
+ this.logInfo('Validating subset inputs...');
300
+
301
+ try {
302
+ const stats = await this.validateSubsetInputs(options.source, options.codes);
303
+
304
+ this.logSuccess('Validation passed');
305
+ console.log(` Source files found: ${stats.filesFound.length}`);
306
+ console.log(` Codes in list: ${stats.codeCount.toLocaleString()}`);
307
+ console.log(` Unique codes: ${stats.uniqueCodes.toLocaleString()}`);
308
+
309
+ if (stats.warnings.length > 0) {
310
+ this.logWarning('Validation warnings:');
311
+ stats.warnings.forEach(warning => console.log(` ${warning}`));
312
+ }
313
+
314
+ } catch (error) {
315
+ this.logError(`Validation failed: ${error.message}`);
316
+ }
317
+ }
318
+
319
+ async validateSubsetPrerequisites(config) {
320
+ const checks = [
321
+ {
322
+ name: 'Source directory exists',
323
+ check: () => {
324
+ const exists = fs.existsSync(config.source);
325
+ if (!exists && config.verbose) {
326
+ console.log(` Source path being checked: ${config.source}`);
327
+ }
328
+ return exists;
329
+ }
330
+ },
331
+ {
332
+ name: 'Codes file exists',
333
+ check: () => {
334
+ const exists = fs.existsSync(config.codes);
335
+ if (!exists && config.verbose) {
336
+ console.log(` Codes path being checked: ${config.codes}`);
337
+ }
338
+ return exists;
339
+ }
340
+ },
341
+ {
342
+ name: 'Source contains LOINC files',
343
+ check: async () => {
344
+ const requiredFiles = [
345
+ 'LoincTable/Loinc.csv',
346
+ 'AccessoryFiles/PartFile/Part.csv'
347
+ ];
348
+
349
+ const results = requiredFiles.map(file => {
350
+ const fullPath = path.join(config.source, file);
351
+ const exists = fs.existsSync(fullPath);
352
+ if (!exists && config.verbose) {
353
+ console.log(` Missing required file: ${fullPath}`);
354
+ }
355
+ return exists;
356
+ });
357
+
358
+ return results.every(result => result);
359
+ }
360
+ }
361
+ ];
362
+
363
+ let allPassed = true;
364
+
365
+ for (const { name, check } of checks) {
366
+ try {
367
+ const passed = await check();
368
+ if (passed) {
369
+ this.logSuccess(name);
370
+ } else {
371
+ this.logError(name);
372
+ allPassed = false;
373
+ }
374
+ } catch (error) {
375
+ this.logError(`${name}: ${error.message}`);
376
+ allPassed = false;
377
+ }
378
+ }
379
+
380
+ return allPassed;
381
+ }
382
+
383
+ async executeSubset(config) {
384
+ this.logInfo('Loading target codes...');
385
+
386
+ // Load the initial target codes
387
+ const initialTargetCodes = await this.loadTargetCodes(config.codes);
388
+ this.logInfo(`Loaded ${initialTargetCodes.size.toLocaleString()} initial target codes`);
389
+
390
+ if (config.verbose) {
391
+ const sampleCodes = Array.from(initialTargetCodes).slice(0, 20);
392
+ console.log(`First 20 codes from file: ${sampleCodes.join(', ')}`);
393
+
394
+ // Validate some codes exist in the main LOINC file
395
+ this.logInfo('Validating codes exist in LOINC...');
396
+ const validationResults = await this.validateCodesExist(config.source, initialTargetCodes);
397
+ console.log(`Found ${validationResults.found} of ${validationResults.checked} codes in LOINC main table`);
398
+ if (validationResults.notFound.length > 0) {
399
+ console.log(`Sample missing codes: ${validationResults.notFound.slice(0, 5).join(', ')}`);
400
+ }
401
+ }
402
+
403
+ let finalTargetCodes = initialTargetCodes;
404
+
405
+ // Expand target codes based on PartLink relationships if requested
406
+ if (config.expandPartLinks) {
407
+ this.logInfo('Expanding target codes based on PartLink relationships...');
408
+ finalTargetCodes = await this.expandCodesFromPartLinks(config.source, initialTargetCodes, config.verbose);
409
+
410
+ const addedCodes = finalTargetCodes.size - initialTargetCodes.size;
411
+ this.logInfo(`Added ${addedCodes.toLocaleString()} related codes from PartLink relationships`);
412
+
413
+ if (config.verbose && addedCodes > 0) {
414
+ const newCodes = Array.from(finalTargetCodes).filter(code => !initialTargetCodes.has(code));
415
+ const sampleNewCodes = newCodes.slice(0, 10);
416
+ console.log(`Sample newly added codes: ${sampleNewCodes.join(', ')}`);
417
+ }
418
+ } else {
419
+ this.logInfo('Skipping PartLink expansion (disabled)');
420
+ }
421
+
422
+ this.logInfo(`Final target codes: ${finalTargetCodes.size.toLocaleString()}`);
423
+
424
+ // Export final codes to file for inspection
425
+ if (config.verbose) {
426
+ const codesOutputPath = path.join(process.cwd(), 'final-target-codes.txt');
427
+ this.logInfo(`Exporting final code set to: ${codesOutputPath}`);
428
+ await this.exportCodesToFile(finalTargetCodes, codesOutputPath);
429
+ }
430
+
431
+ // Create subset processor
432
+ const processor = new LoincSubsetProcessor(this, config.verbose);
433
+
434
+ await processor.createSubset(
435
+ config.source,
436
+ config.dest,
437
+ finalTargetCodes,
438
+ {
439
+ verbose: config.verbose,
440
+ overwrite: config.overwrite,
441
+ originalCodes: initialTargetCodes // Pass original codes for PartLink filtering
442
+ }
443
+ );
444
+ }
445
+
446
+ async expandCodesFromPartLinks(sourceDir, initialCodes, verbose = false) {
447
+ const partLinkPath = path.join(sourceDir, 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv');
448
+
449
+ if (!fs.existsSync(partLinkPath)) {
450
+ if (verbose) {
451
+ console.log(` PartLink file not found: ${partLinkPath}`);
452
+ }
453
+ return new Set(initialCodes);
454
+ }
455
+
456
+ const expandedCodes = new Set(initialCodes);
457
+
458
+ if (verbose) {
459
+ console.log(` Processing PartLink relationships (single pass)...`);
460
+ console.log(` Original codes count: ${initialCodes.size}`);
461
+ }
462
+
463
+ const rl = readline.createInterface({
464
+ input: fs.createReadStream(partLinkPath),
465
+ crlfDelay: Infinity
466
+ });
467
+
468
+ let lineNum = 0;
469
+ let addedCodes = 0;
470
+ let matchedLines = 0;
471
+
472
+ for await (const line of rl) {
473
+ lineNum++;
474
+
475
+ // Skip header
476
+ if (lineNum === 1) continue;
477
+
478
+ const items = this.csvSplit(line, 7);
479
+ if (items.length < 3) continue;
480
+
481
+ const firstCode = this.removeQuotes(items[0]); // First cell
482
+ const thirdCode = this.removeQuotes(items[2]); // Third cell
483
+
484
+ // If either code is in our INITIAL target set, add both codes
485
+ const firstInOriginal = initialCodes.has(firstCode);
486
+ const thirdInOriginal = initialCodes.has(thirdCode);
487
+
488
+ if (firstInOriginal || thirdInOriginal) {
489
+ matchedLines++;
490
+ const sizeBefore = expandedCodes.size;
491
+ expandedCodes.add(firstCode);
492
+ expandedCodes.add(thirdCode);
493
+ const sizeAfter = expandedCodes.size;
494
+ addedCodes += (sizeAfter - sizeBefore);
495
+
496
+ if (verbose && matchedLines <= 10) {
497
+ console.log(` Match ${matchedLines}: "${firstCode}" (${firstInOriginal ? 'in original' : 'new'}) <-> "${thirdCode}" (${thirdInOriginal ? 'in original' : 'new'})`);
498
+ }
499
+ }
500
+ }
501
+
502
+ if (verbose) {
503
+ console.log(` Single pass completed: ${matchedLines} matching relationships found`);
504
+ console.log(` Added ${addedCodes} new codes`);
505
+ console.log(` Final expanded codes count: ${expandedCodes.size}`);
506
+
507
+ // Show what percentage this represents
508
+ const expansionRatio = expandedCodes.size / initialCodes.size;
509
+ console.log(` Expansion ratio: ${expansionRatio.toFixed(1)}x original size`);
510
+ }
511
+
512
+ return expandedCodes;
513
+ }
514
+
515
+ // Helper method for CSV parsing (moved from processor for reuse)
516
+ csvSplit(line, expectedCount) {
517
+ const result = new Array(expectedCount).fill('');
518
+ let inQuoted = false;
519
+ let currentField = 0;
520
+ let fieldStart = 0;
521
+ let i = 0;
522
+
523
+ while (i < line.length && currentField < expectedCount) {
524
+ const ch = line[i];
525
+
526
+ if (!inQuoted && ch === ',') {
527
+ if (currentField < expectedCount) {
528
+ result[currentField] = line.substring(fieldStart, i);
529
+ currentField++;
530
+ fieldStart = i + 1;
531
+ }
532
+ } else if (ch === '"') {
533
+ if (inQuoted && i + 1 < line.length && line[i + 1] === '"') {
534
+ i++; // Skip escaped quote
535
+ } else {
536
+ inQuoted = !inQuoted;
537
+ }
538
+ }
539
+ i++;
540
+ }
541
+
542
+ // Handle last field
543
+ if (currentField < expectedCount) {
544
+ result[currentField] = line.substring(fieldStart);
545
+ }
546
+
547
+ return result;
548
+ }
549
+
550
+ // Helper method for removing quotes (moved from processor for reuse)
551
+ removeQuotes(str) {
552
+ if (!str) return '';
553
+ return str.replace(/^"|"$/g, '');
554
+ }
555
+
556
+ async exportCodesToFile(codeSet, filePath) {
557
+ const sortedCodes = Array.from(codeSet).sort();
558
+ const content = sortedCodes.join('\n') + '\n';
559
+
560
+ fs.writeFileSync(filePath, content, 'utf8');
561
+ this.logInfo(`Exported ${sortedCodes.length.toLocaleString()} codes to ${filePath}`);
562
+ }
563
+
564
+ async validateCodesExist(sourceDir, targetCodes) {
565
+ const loincMainPath = path.join(sourceDir, 'LoincTable/Loinc.csv');
566
+
567
+ if (!fs.existsSync(loincMainPath)) {
568
+ return { found: 0, checked: 0, notFound: [] };
569
+ }
570
+
571
+ const foundCodes = new Set();
572
+
573
+ const rl = readline.createInterface({
574
+ input: fs.createReadStream(loincMainPath),
575
+ crlfDelay: Infinity
576
+ });
577
+
578
+ let lineNum = 0;
579
+ for await (const line of rl) {
580
+ lineNum++;
581
+
582
+ // Skip header
583
+ if (lineNum === 1) continue;
584
+
585
+ const items = this.csvSplit(line, 39);
586
+ if (items.length < 1) continue;
587
+
588
+ const code = this.removeQuotes(items[0]);
589
+ if (targetCodes.has(code)) {
590
+ foundCodes.add(code);
591
+ }
592
+ }
593
+
594
+ const checked = Math.min(targetCodes.size, 100); // Only check first 100 for performance
595
+ const notFound = Array.from(targetCodes).slice(0, 100).filter(code => !foundCodes.has(code));
596
+
597
+ return {
598
+ found: foundCodes.size,
599
+ checked: checked,
600
+ notFound: notFound
601
+ };
602
+ }
603
+
604
+ async loadTargetCodes(codesFile) {
605
+ const codes = new Set();
606
+
607
+ const rl = readline.createInterface({
608
+ input: fs.createReadStream(codesFile),
609
+ crlfDelay: Infinity
610
+ });
611
+
612
+ for await (const line of rl) {
613
+ const code = line.trim();
614
+ if (code && !code.startsWith('#')) { // Allow comments with #
615
+ codes.add(code);
616
+ }
617
+ }
618
+
619
+ return codes;
620
+ }
621
+
622
+ async validateSubsetInputs(sourceDir, codesFile) {
623
+ const stats = {
624
+ filesFound: [],
625
+ codeCount: 0,
626
+ uniqueCodes: 0,
627
+ warnings: []
628
+ };
629
+
630
+ // Check for LOINC files
631
+ const loincFiles = [
632
+ 'LoincTable/Loinc.csv',
633
+ 'AccessoryFiles/PartFile/Part.csv',
634
+ 'AccessoryFiles/ConsumerName/ConsumerName.csv',
635
+ 'AccessoryFiles/AnswerFile/AnswerList.csv',
636
+ 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
637
+ 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
638
+ 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
639
+ 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv'
640
+ ];
641
+
642
+ for (const file of loincFiles) {
643
+ const filePath = path.join(sourceDir, file);
644
+ if (fs.existsSync(filePath)) {
645
+ stats.filesFound.push(file);
646
+ } else {
647
+ stats.warnings.push(`File not found: ${file}`);
648
+ }
649
+ }
650
+
651
+ // Validate codes file
652
+ const codes = await this.loadTargetCodes(codesFile);
653
+ stats.codeCount = codes.size;
654
+ stats.uniqueCodes = codes.size;
655
+
656
+ return stats;
657
+ }
658
+ }
659
+
660
+ class LoincSubsetProcessor {
661
+ constructor(moduleInstance, verbose = true) {
662
+ this.module = moduleInstance;
663
+ this.verbose = verbose;
664
+ this.targetCodes = null;
665
+ this.processedFiles = 0;
666
+ this.totalFiles = 0;
667
+ }
668
+
669
+ // Add this method to LoincSubsetProcessor
670
+ cleanLine(line) {
671
+ // Remove trailing commas and whitespace
672
+ return line.replace(/,+\s*$/, '');
673
+ }
674
+
675
+ async createSubset(sourceDir, destDir, targetCodes, options) {
676
+ this.targetCodes = targetCodes;
677
+ this.originalCodes = options.originalCodes || targetCodes; // Fallback to targetCodes if not provided
678
+
679
+ // Create destination directory structure
680
+ await this.createDirectoryStructure(destDir, options.overwrite);
681
+
682
+ // Define files to process with their handlers
683
+ const filesToProcess = [
684
+ {
685
+ source: 'LoincTable/Loinc.csv',
686
+ dest: 'LoincTable/Loinc.csv',
687
+ handler: 'processMainCodes'
688
+ },
689
+ {
690
+ source: 'AccessoryFiles/PartFile/Part.csv',
691
+ dest: 'AccessoryFiles/PartFile/Part.csv',
692
+ handler: 'processParts'
693
+ },
694
+ {
695
+ source: 'AccessoryFiles/ConsumerName/ConsumerName.csv',
696
+ dest: 'AccessoryFiles/ConsumerName/ConsumerName.csv',
697
+ handler: 'processConsumerNames'
698
+ },
699
+ {
700
+ source: 'AccessoryFiles/AnswerFile/AnswerList.csv',
701
+ dest: 'AccessoryFiles/AnswerFile/AnswerList.csv',
702
+ handler: 'processAnswerLists'
703
+ },
704
+ {
705
+ source: 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
706
+ dest: 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
707
+ handler: 'processPartLinks'
708
+ },
709
+ {
710
+ source: 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
711
+ dest: 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
712
+ handler: 'processAnswerListLinks'
713
+ },
714
+ {
715
+ source: 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
716
+ dest: 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
717
+ handler: 'processHierarchy'
718
+ },
719
+ {
720
+ source: 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
721
+ dest: 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
722
+ handler: 'processLanguageVariants'
723
+ }
724
+ ];
725
+
726
+ // Count existing files
727
+ this.totalFiles = filesToProcess.filter(file =>
728
+ fs.existsSync(path.join(sourceDir, file.source))
729
+ ).length;
730
+
731
+ // Add language variant files
732
+ const languageVariantFiles = await this.findLanguageVariantFiles(sourceDir);
733
+ this.totalFiles += languageVariantFiles.length;
734
+
735
+ this.module.logInfo(`Processing ${this.totalFiles} files...`);
736
+ this.module.createProgressBar();
737
+ this.module.updateProgress(0, this.totalFiles);
738
+
739
+ // Process main files
740
+ for (const file of filesToProcess) {
741
+ const sourcePath = path.join(sourceDir, file.source);
742
+ const destPath = path.join(destDir, file.dest);
743
+
744
+ if (fs.existsSync(sourcePath)) {
745
+ if (this.verbose) {
746
+ this.module.logInfo(`Processing ${file.source}...`);
747
+ }
748
+
749
+ await this[file.handler](sourcePath, destPath);
750
+ this.processedFiles++;
751
+ this.module.updateProgress(this.processedFiles);
752
+ }
753
+ }
754
+
755
+ // Process language variant files
756
+ for (const langFile of languageVariantFiles) {
757
+ const sourcePath = path.join(sourceDir, langFile);
758
+ const destPath = path.join(destDir, langFile);
759
+
760
+ if (this.verbose) {
761
+ this.module.logInfo(`Processing ${langFile}...`);
762
+ }
763
+
764
+ await this.processLanguageVariantFile(sourcePath, destPath);
765
+ this.processedFiles++;
766
+ this.module.updateProgress(this.processedFiles);
767
+ }
768
+
769
+ this.module.stopProgress();
770
+ }
771
+
772
+ async createDirectoryStructure(destDir, overwrite) {
773
+ if (fs.existsSync(destDir)) {
774
+ if (overwrite) {
775
+ fs.rmSync(destDir, { recursive: true, force: true });
776
+ } else {
777
+ throw new Error(`Destination directory already exists: ${destDir}`);
778
+ }
779
+ }
780
+
781
+ // Create directory structure
782
+ const dirs = [
783
+ destDir,
784
+ path.join(destDir, 'LoincTable'),
785
+ path.join(destDir, 'AccessoryFiles'),
786
+ path.join(destDir, 'AccessoryFiles/PartFile'),
787
+ path.join(destDir, 'AccessoryFiles/ConsumerName'),
788
+ path.join(destDir, 'AccessoryFiles/AnswerFile'),
789
+ path.join(destDir, 'AccessoryFiles/ComponentHierarchyBySystem'),
790
+ path.join(destDir, 'AccessoryFiles/LinguisticVariants')
791
+ ];
792
+
793
+ for (const dir of dirs) {
794
+ fs.mkdirSync(dir, { recursive: true });
795
+ }
796
+ }
797
+
798
+ async findLanguageVariantFiles(sourceDir) {
799
+ const languageVariantFiles = [];
800
+ const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
801
+
802
+ if (fs.existsSync(linguisticVariantsDir)) {
803
+ const files = fs.readdirSync(linguisticVariantsDir);
804
+ for (const file of files) {
805
+ if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
806
+ languageVariantFiles.push(`AccessoryFiles/LinguisticVariants/${file}`);
807
+ }
808
+ }
809
+ }
810
+
811
+ return languageVariantFiles;
812
+ }
813
+
814
+ async processMainCodes(sourcePath, destPath) {
815
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
816
+ if (lineNum === 1) return true; // Keep header
817
+
818
+ const items = this.csvSplit(line, 39);
819
+ if (items.length < 1) return false;
820
+
821
+ const code = this.removeQuotes(items[0]);
822
+ return this.targetCodes.has(code);
823
+ });
824
+ }
825
+
826
+ async processParts(sourcePath, destPath) {
827
+ if (this.verbose) {
828
+ console.log(` Processing parts file: ${sourcePath}`);
829
+ console.log(` Target codes size: ${this.targetCodes.size}`);
830
+
831
+ // Show first few target codes for comparison
832
+ const sampleCodes = Array.from(this.targetCodes).slice(0, 5);
833
+ console.log(` Sample target codes: ${sampleCodes.join(', ')}`);
834
+ }
835
+
836
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
837
+ if (lineNum === 1) return true; // Keep header
838
+
839
+ const items = this.csvSplit(line, 5);
840
+ if (items.length < 1) return false;
841
+
842
+ const code = this.removeQuotes(items[0]);
843
+ const hasCode = this.targetCodes.has(code);
844
+
845
+ // Debug first few lines
846
+ if (this.verbose && lineNum <= 5) {
847
+ console.log(` Line ${lineNum}: raw="${items[0]}", clean="${code}", match=${hasCode}`);
848
+ }
849
+
850
+ return hasCode;
851
+ });
852
+ }
853
+
854
+ async processConsumerNames(sourcePath, destPath) {
855
+ if (this.verbose) {
856
+ console.log(` Processing consumer names file: ${sourcePath}`);
857
+ console.log(` Target codes size: ${this.targetCodes.size}`);
858
+
859
+ // Show first few target codes for comparison
860
+ const sampleCodes = Array.from(this.targetCodes).slice(0, 5);
861
+ console.log(` Sample target codes: ${sampleCodes.join(', ')}`);
862
+ }
863
+
864
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
865
+ if (lineNum === 1) return true; // Keep header
866
+
867
+ const items = this.csvSplit(line, 2);
868
+ if (items.length < 1) return false;
869
+
870
+ const code = this.removeQuotes(items[0]);
871
+ const hasCode = this.targetCodes.has(code);
872
+
873
+ // Debug first few lines
874
+ if (this.verbose && lineNum <= 5) {
875
+ console.log(` Line ${lineNum}: raw="${items[0]}", clean="${code}", match=${hasCode}`);
876
+ }
877
+
878
+ return hasCode;
879
+ });
880
+ }
881
+
882
+ async processAnswerLists(sourcePath, destPath) {
883
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
884
+ if (lineNum === 1) return true; // Keep header
885
+
886
+ const items = this.csvSplit(line, 11);
887
+ if (items.length < 7) return false;
888
+
889
+ const listCode = this.removeQuotes(items[0]);
890
+ const answerCode = this.removeQuotes(items[6]);
891
+
892
+ return this.targetCodes.has(listCode) || this.targetCodes.has(answerCode);
893
+ });
894
+ }
895
+
896
+ async processPartLinks(sourcePath, destPath) {
897
+ if (this.verbose) {
898
+ console.log(` Processing part links file: ${sourcePath}`);
899
+ console.log(` Target codes size: ${this.targetCodes.size}`);
900
+ console.log(` Original codes size: ${this.originalCodes.size}`);
901
+ console.log(` Using ORIGINAL codes for PartLink filtering to prevent expansion explosion`);
902
+ }
903
+
904
+ let debugCount = 0;
905
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
906
+ if (lineNum === 1) return true; // Keep header
907
+
908
+ const items = this.csvSplit(line, 7);
909
+ if (items.length < 3) return false;
910
+
911
+ const sourceCode = this.removeQuotes(items[0]); // First cell
912
+ const targetCode = this.removeQuotes(items[2]); // Third cell
913
+
914
+ // Use ORIGINAL codes for filtering, not expanded codes
915
+ const hasSource = this.originalCodes.has(sourceCode);
916
+ const hasTarget = this.originalCodes.has(targetCode);
917
+ const shouldInclude = hasSource || hasTarget;
918
+
919
+ // Debug first few included lines
920
+ if (this.verbose && shouldInclude && debugCount < 10) {
921
+ debugCount++;
922
+ console.log(` Include ${debugCount}: "${sourceCode}" (${hasSource ? 'original' : 'no'}) <-> "${targetCode}" (${hasTarget ? 'original' : 'no'})`);
923
+ }
924
+
925
+ return shouldInclude;
926
+ });
927
+ }
928
+
929
+ async processAnswerListLinks(sourcePath, destPath) {
930
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
931
+ if (lineNum === 1) return true; // Keep header
932
+
933
+ const items = this.csvSplit(line, 7); // Increased expected count to handle 6th column
934
+ if (items.length < 6) return false;
935
+
936
+ const firstCode = this.removeQuotes(items[0]); // First cell
937
+ const thirdCode = this.removeQuotes(items[2]); // Third cell
938
+ const sixthCode = this.removeQuotes(items[5]); // Sixth cell (index 5)
939
+
940
+ const res = this.targetCodes.has(firstCode) && (
941
+ this.targetCodes.has(thirdCode) && (!sixthCode || this.targetCodes.has(sixthCode)));
942
+ return res;
943
+ });
944
+ }
945
+
946
+ async processHierarchy(sourcePath, destPath) {
947
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
948
+ if (lineNum === 1) return true; // Keep header
949
+
950
+ const items = this.csvSplit(line, 12);
951
+ if (items.length < 5) return false;
952
+
953
+ const childCode = this.removeQuotes(items[2]); // Third cell (column 3)
954
+ const relatedCode = this.removeQuotes(items[3]); // Fourth cell (column 4)
955
+
956
+ if (!childCode) {
957
+ return true;
958
+ }
959
+ // Check if this row should be included based on columns 3 and 4
960
+ if (this.targetCodes.has(childCode) && this.targetCodes.has(relatedCode)) {
961
+ // Modify the hierarchical path in column 1 (first cell)
962
+ const originalPath = this.removeQuotes(items[0]);
963
+ const pathCodes = originalPath.split('.');
964
+ const filteredCodes = pathCodes.filter(code => this.targetCodes.has(code));
965
+ const newPath = filteredCodes.join('.');
966
+
967
+ // Rebuild the line with modified path
968
+ const modifiedItems = [...items];
969
+ modifiedItems[0] = `"${newPath}"`;
970
+ return { include: true, modifiedLine: modifiedItems.join(',') };
971
+ }
972
+
973
+ return false;
974
+ });
975
+ }
976
+
977
+ async processLanguageVariants(sourcePath, destPath) {
978
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
979
+ if (lineNum === 1) return true; // Keep header
980
+
981
+ // For the main LinguisticVariants.csv, include all language definitions
982
+ // since we can't know which languages we'll need until we process individual files
983
+ return true;
984
+ });
985
+ }
986
+
987
+ async processLanguageVariantFile(sourcePath, destPath) {
988
+ await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
989
+ if (lineNum === 1) return true; // Keep header
990
+
991
+ const items = this.csvSplit(line, 12);
992
+ if (items.length < 1) return false;
993
+
994
+ // First column contains the LOINC code
995
+ const code = this.removeQuotes(items[0]);
996
+ return this.targetCodes.has(code);
997
+ });
998
+ }
999
+
1000
+ async processFileWithFilter(sourcePath, destPath, filterFunction) {
1001
+ if (!fs.existsSync(sourcePath)) {
1002
+ return;
1003
+ }
1004
+
1005
+ const readStream = fs.createReadStream(sourcePath);
1006
+ const writeStream = fs.createWriteStream(destPath);
1007
+
1008
+ const rl = readline.createInterface({
1009
+ input: readStream,
1010
+ crlfDelay: Infinity
1011
+ });
1012
+
1013
+ let lineNum = 0;
1014
+ let includedLines = 0;
1015
+
1016
+ for await (const line of rl) {
1017
+ lineNum++;
1018
+
1019
+ const result = filterFunction(line, lineNum);
1020
+
1021
+ if (result === true) {
1022
+ const cleanedLine = this.cleanLine(line);
1023
+ writeStream.write(cleanedLine + '\n');
1024
+ includedLines++;
1025
+ } else if (result && result.include) {
1026
+ const cleanedLine = this.cleanLine(result.modifiedLine);
1027
+ writeStream.write(cleanedLine + '\n');
1028
+ includedLines++;
1029
+ }
1030
+ }
1031
+
1032
+ writeStream.end();
1033
+
1034
+ if (this.verbose && lineNum > 1) {
1035
+ console.log(` Included ${includedLines - 1} of ${lineNum - 1} data rows`);
1036
+ }
1037
+ }
1038
+
1039
+ csvSplit(line, expectedCount) {
1040
+ const result = new Array(expectedCount).fill('');
1041
+ let inQuoted = false;
1042
+ let currentField = 0;
1043
+ let fieldStart = 0;
1044
+ let i = 0;
1045
+
1046
+ while (i < line.length && currentField < expectedCount) {
1047
+ const ch = line[i];
1048
+
1049
+ if (!inQuoted && ch === ',') {
1050
+ if (currentField < expectedCount) {
1051
+ result[currentField] = line.substring(fieldStart, i);
1052
+ currentField++;
1053
+ fieldStart = i + 1;
1054
+ }
1055
+ } else if (ch === '"') {
1056
+ if (inQuoted && i + 1 < line.length && line[i + 1] === '"') {
1057
+ i++; // Skip escaped quote
1058
+ } else {
1059
+ inQuoted = !inQuoted;
1060
+ }
1061
+ }
1062
+ i++;
1063
+ }
1064
+
1065
+ // Handle last field
1066
+ if (currentField < expectedCount) {
1067
+ result[currentField] = line.substring(fieldStart);
1068
+ }
1069
+
1070
+ return result;
1071
+ }
1072
+
1073
+ removeQuotes(str) {
1074
+ if (!str) return '';
1075
+ return str.replace(/^"|"$/g, '');
1076
+ }
1077
+ }
1078
+
1079
+ module.exports = {
1080
+ LoincSubsetModule
1081
+ };