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.
- package/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- 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
|
+
};
|