fhirsmith 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. package/xig/xig.js +3049 -0
@@ -0,0 +1,2077 @@
1
+ /**
2
+ * SNOMED CT Expression Library
3
+ * Converted from Pascal ftx_sct_expressions.pas
4
+ *
5
+ * Copyright (c) 2011+, HL7 and Health Intersections Pty Ltd
6
+ * Licensed under BSD-3-Clause
7
+ */
8
+
9
+ const MAX_TERM_LENGTH = 1024;
10
+ const NO_REFERENCE = 0xFFFFFFFF;
11
+
12
+ // Expression status enumeration
13
+ const SnomedExpressionStatus = {
14
+ Unknown: 0,
15
+ Equivalent: 1,
16
+ SubsumedBy: 2
17
+ };
18
+
19
+ /**
20
+ * Base class for all SNOMED expression elements
21
+ */
22
+ class SnomedExpressionBase {
23
+ constructor() {
24
+ this.start = 0;
25
+ this.stop = 0;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Represents a SNOMED concept with optional code, description, literal, or decimal value
31
+ */
32
+ class SnomedConcept extends SnomedExpressionBase {
33
+ constructor(reference = NO_REFERENCE) {
34
+ super();
35
+ this.reference = reference;
36
+ this.code = '';
37
+ this.description = '';
38
+ this.literal = '';
39
+ this.decimal = '';
40
+ }
41
+
42
+ /**
43
+ * Check if this concept matches another concept
44
+ */
45
+ matches(other) {
46
+ if (!other) return false;
47
+
48
+ if (this.reference !== NO_REFERENCE) {
49
+ return this.reference === other.reference;
50
+ } else if (this.code) {
51
+ return this.code === other.code;
52
+ } else if (this.decimal) {
53
+ return this.decimal === other.decimal;
54
+ } else if (this.literal) {
55
+ return this.literal === other.literal;
56
+ }
57
+
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Get a string description of this concept
63
+ */
64
+ describe() {
65
+ if (this.code) {
66
+ return this.code;
67
+ } else if (this.decimal) {
68
+ return '#' + this.decimal;
69
+ } else if (this.literal) {
70
+ return '"' + this.literal + '"';
71
+ }
72
+ return '';
73
+ }
74
+
75
+ /**
76
+ * Compare two concepts for sorting
77
+ */
78
+ compare(other) {
79
+ if (this.code) {
80
+ return this.code.localeCompare(other.code);
81
+ } else if (this.decimal) {
82
+ return this.decimal.localeCompare(other.decimal);
83
+ } else {
84
+ return this.literal.localeCompare(other.literal);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create a canonical copy of this concept
90
+ */
91
+ canonical() {
92
+ const result = new SnomedConcept();
93
+ result.copyFrom(this);
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Copy properties from another concept
99
+ */
100
+ copyFrom(other) {
101
+ this.reference = other.reference;
102
+ this.code = other.code;
103
+ this.description = other.description;
104
+ this.literal = other.literal;
105
+ this.decimal = other.decimal;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Represents a refinement (attribute-value pair) in a SNOMED expression
111
+ */
112
+ class SnomedRefinement extends SnomedExpressionBase {
113
+ constructor() {
114
+ super();
115
+ this.name = null; // SnomedConcept
116
+ this.value = null; // SnomedExpression
117
+ }
118
+
119
+ /**
120
+ * Set the name (attribute) of this refinement
121
+ */
122
+ setName(name) {
123
+ this.name = name;
124
+ }
125
+
126
+ /**
127
+ * Set the value of this refinement
128
+ */
129
+ setValue(value) {
130
+ this.value = value;
131
+ }
132
+
133
+ /**
134
+ * Check if this refinement matches another
135
+ */
136
+ matches(other) {
137
+ if (!other) return false;
138
+ if (!this.name.matches(other.name)) return false;
139
+ return this.value.matches(other.value) === '';
140
+ }
141
+
142
+ /**
143
+ * Get a string description of this refinement
144
+ */
145
+ describe() {
146
+ return this.name.describe() + '=' + this.value.describe();
147
+ }
148
+
149
+ /**
150
+ * Compare two refinements for sorting
151
+ */
152
+ compare(other) {
153
+ return this.name.compare(other.name);
154
+ }
155
+
156
+ /**
157
+ * Create a canonical copy of this refinement
158
+ */
159
+ canonical() {
160
+ const result = new SnomedRefinement();
161
+ result.name = this.name.canonical();
162
+ result.value = this.value.canonical();
163
+ return result;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Represents a group of refinements in a SNOMED expression
169
+ */
170
+ class SnomedRefinementGroup extends SnomedExpressionBase {
171
+ constructor() {
172
+ super();
173
+ this.refinements = []; // Array of SnomedRefinement
174
+ }
175
+
176
+ /**
177
+ * Check if this group matches another group
178
+ */
179
+ matches(other) {
180
+ if (!other) return false;
181
+
182
+ // Check all refinements in this group exist in other
183
+ for (const refinement of this.refinements) {
184
+ if (!other.hasRefinement(refinement)) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ // Check all refinements in other group exist in this
190
+ for (const refinement of other.refinements) {
191
+ if (!this.hasRefinement(refinement)) {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ return true;
197
+ }
198
+
199
+ /**
200
+ * Check if this group contains a specific refinement
201
+ */
202
+ hasRefinement(refinement) {
203
+ return this.refinements.some(test => test.matches(refinement));
204
+ }
205
+
206
+ /**
207
+ * Get a string description of this refinement group
208
+ */
209
+ describe() {
210
+ return this.refinements.map(r => r.describe()).join(',');
211
+ }
212
+
213
+ /**
214
+ * Compare two refinement groups for sorting
215
+ */
216
+ compare(other) {
217
+ if (this.refinements.length === 0 || other.refinements.length === 0) {
218
+ return this.refinements.length - other.refinements.length;
219
+ }
220
+ return this.refinements[0].compare(other.refinements[0]);
221
+ }
222
+
223
+ /**
224
+ * Create a canonical copy of this refinement group
225
+ */
226
+ canonical() {
227
+ const result = new SnomedRefinementGroup();
228
+ for (const refinement of this.refinements) {
229
+ result.refinements.push(refinement.canonical());
230
+ }
231
+ return result;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Main SNOMED expression class containing concepts, refinements, and refinement groups
237
+ */
238
+ class SnomedExpression extends SnomedExpressionBase {
239
+ constructor() {
240
+ super();
241
+ this.status = SnomedExpressionStatus.Unknown;
242
+ this.concepts = []; // Array of SnomedConcept
243
+ this.refinements = []; // Array of SnomedRefinement
244
+ this.refinementGroups = []; // Array of SnomedRefinementGroup
245
+ }
246
+
247
+ /**
248
+ * Check if expression has refinements
249
+ */
250
+ hasRefinements() {
251
+ return this.refinements.length > 0;
252
+ }
253
+
254
+ /**
255
+ * Check if expression has refinement groups
256
+ */
257
+ hasRefinementGroups() {
258
+ return this.refinementGroups.length > 0;
259
+ }
260
+
261
+ /**
262
+ * Check if this is a simple expression (single concept, no refinements)
263
+ */
264
+ isSimple() {
265
+ return (this.concepts.length === 1) &&
266
+ !this.hasRefinements() &&
267
+ !this.hasRefinementGroups();
268
+ }
269
+
270
+ /**
271
+ * Check if this is a complex expression
272
+ */
273
+ isComplex() {
274
+ return !this.isSimple();
275
+ }
276
+
277
+ /**
278
+ * Check if expression contains a specific concept
279
+ */
280
+ hasConcept(concept) {
281
+ if (typeof concept === 'number') {
282
+ return this.concepts.some(test => test.reference === concept);
283
+ } else {
284
+ return this.concepts.some(test => test.matches(concept));
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Check if expression contains a specific refinement
290
+ */
291
+ hasRefinement(refinement) {
292
+ return this.refinements.some(test => test.matches(refinement));
293
+ }
294
+
295
+ /**
296
+ * Check if expression contains a specific refinement group
297
+ */
298
+ hasRefinementGroup(refinementGroup) {
299
+ return this.refinementGroups.some(test => test.matches(refinementGroup));
300
+ }
301
+
302
+ /**
303
+ * Check if this expression matches another expression
304
+ * Returns empty string if match, error message if not
305
+ */
306
+ matches(other) {
307
+ if (!other) return 'other is nil';
308
+
309
+ // Check concepts
310
+ for (const concept of this.concepts) {
311
+ if (!other.hasConcept(concept)) {
312
+ return 'concept ' + concept.describe() + ' not found in first expression';
313
+ }
314
+ }
315
+ for (const concept of other.concepts) {
316
+ if (!this.hasConcept(concept)) {
317
+ return 'concept ' + concept.describe() + ' not found in second expression';
318
+ }
319
+ }
320
+
321
+ // Check refinements
322
+ for (const refinement of this.refinements) {
323
+ if (!other.hasRefinement(refinement)) {
324
+ return 'refinement ' + refinement.describe() + ' not found in first expression';
325
+ }
326
+ }
327
+ for (const refinement of other.refinements) {
328
+ if (!this.hasRefinement(refinement)) {
329
+ return 'refinement ' + refinement.describe() + ' not found in second expression';
330
+ }
331
+ }
332
+
333
+ // Check refinement groups
334
+ for (const group of this.refinementGroups) {
335
+ if (!other.hasRefinementGroup(group)) {
336
+ return 'refinement group ' + group.describe() + ' not found in first expression';
337
+ }
338
+ }
339
+ for (const group of other.refinementGroups) {
340
+ if (!this.hasRefinementGroup(group)) {
341
+ return 'refinement group ' + group.describe() + ' not found in second expression';
342
+ }
343
+ }
344
+
345
+ return '';
346
+ }
347
+
348
+ /**
349
+ * Get a string description of this expression
350
+ */
351
+ describe() {
352
+ const parts = [];
353
+
354
+ // Status prefix
355
+ switch (this.status) {
356
+ case SnomedExpressionStatus.Equivalent:
357
+ parts.push('===');
358
+ break;
359
+ case SnomedExpressionStatus.SubsumedBy:
360
+ parts.push('<<<');
361
+ break;
362
+ }
363
+
364
+ // Concepts
365
+ parts.push(...this.concepts.map(c => c.describe()));
366
+
367
+ // Refinements
368
+ parts.push(...this.refinements.map(r => r.describe()));
369
+
370
+ // Refinement groups
371
+ parts.push(...this.refinementGroups.map(g => g.describe()));
372
+
373
+ return parts.join(',');
374
+ }
375
+
376
+ /**
377
+ * Create a canonical form of this expression (sorted and normalized)
378
+ */
379
+ canonical() {
380
+ const result = new SnomedExpression();
381
+ result.status = this.status;
382
+
383
+ // Copy and sort concepts
384
+ for (const concept of this.concepts) {
385
+ result.concepts.push(concept.canonical());
386
+ }
387
+
388
+ // Convert refinements and groups to canonical form
389
+ if (this.hasRefinementGroups() || this.hasRefinements()) {
390
+ // Copy refinement groups
391
+ for (const group of this.refinementGroups) {
392
+ if (group.refinements.length > 0) {
393
+ result.refinementGroups.push(group.canonical());
394
+ }
395
+ }
396
+
397
+ // Convert individual refinements to single-refinement groups
398
+ for (const refinement of this.refinements) {
399
+ const group = new SnomedRefinementGroup();
400
+ group.refinements.push(refinement.canonical());
401
+ result.refinementGroups.push(group);
402
+ }
403
+ }
404
+
405
+ // Sort everything
406
+ result.concepts.sort((a, b) => a.compare(b));
407
+
408
+ if (result.hasRefinementGroups()) {
409
+ for (const group of result.refinementGroups) {
410
+ group.refinements.sort((a, b) => a.compare(b));
411
+ }
412
+ result.refinementGroups.sort((a, b) => a.compare(b));
413
+ }
414
+
415
+ return result;
416
+ }
417
+
418
+ /**
419
+ * Merge another expression into this one
420
+ */
421
+ merge(exp) {
422
+ if (exp) {
423
+ this.concepts.push(...exp.concepts);
424
+ this.refinements.push(...exp.refinements);
425
+ this.refinementGroups.push(...exp.refinementGroups);
426
+ }
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Parser for SNOMED CT expression strings
432
+ */
433
+ class SnomedExpressionParser {
434
+ /**
435
+ * @param {SnomedConceptList} conceptList - list of all snomed concepts
436
+ */
437
+ constructor(conceptList) {
438
+ this.source = '';
439
+ this.cursor = 0;
440
+ this.conceptList = conceptList;
441
+ }
442
+
443
+ /**
444
+ * Parse a SNOMED expression string
445
+ */
446
+ parse(source) {
447
+ this.source = source;
448
+ this.cursor = 0;
449
+
450
+ const result = new SnomedExpression();
451
+ result.start = this.cursor;
452
+
453
+ this.ws();
454
+
455
+ // Check for status prefix
456
+ if (this.peek() === '=') {
457
+ result.status = SnomedExpressionStatus.Equivalent;
458
+ this.prefix('=');
459
+ } else if (this.peek() === '<') {
460
+ result.status = SnomedExpressionStatus.SubsumedBy;
461
+ this.prefix('<');
462
+ }
463
+
464
+ // Parse main expression
465
+ result.concepts.push(this.concept());
466
+ while (this.gchar('+')) {
467
+ result.concepts.push(this.concept());
468
+ }
469
+
470
+ if (this.gchar(':')) {
471
+ this.ws();
472
+ this.refinements(result);
473
+ }
474
+
475
+ result.stop = this.cursor;
476
+
477
+ this.rule(this.peek() === '\0', 'Found content ("' + this.peekDisp() + '") after end of expression');
478
+
479
+ return result;
480
+ }
481
+
482
+ /**
483
+ * Parse a concept
484
+ */
485
+ concept() {
486
+ const result = new SnomedConcept();
487
+ result.start = this.cursor;
488
+
489
+ this.ws();
490
+
491
+ if (this.peek() === '#') {
492
+ result.decimal = this.decimal();
493
+ } else if (this.peek() === '"') {
494
+ result.literal = this.stringConstant();
495
+ } else {
496
+ result.code = this.conceptId();
497
+ if (this.conceptList) {
498
+ const found = this.conceptList.findConcept(result.code)
499
+ this.rule(found.found, 'Concept "' + result.code + '" not valid');
500
+ result.reference = found.index;
501
+ } else {
502
+ result.reference = 0;
503
+ }
504
+ }
505
+
506
+ this.ws();
507
+
508
+ if (this.gchar('|')) {
509
+ this.ws();
510
+ result.description = this.term().trim();
511
+ this.ws();
512
+ this.fixed('|');
513
+ this.ws();
514
+ }
515
+
516
+ result.stop = this.cursor;
517
+ return result;
518
+ }
519
+
520
+ /**
521
+ * Parse refinements for an expression
522
+ */
523
+ refinements(expr) {
524
+ let next = true;
525
+ while (next) {
526
+ if (this.peek() !== '{') {
527
+ expr.refinements.push(this.attribute());
528
+ } else {
529
+ expr.refinementGroups.push(this.attributeGroup());
530
+ }
531
+ this.ws();
532
+ next = this.gchar(',');
533
+ this.ws();
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Parse an attribute group
539
+ */
540
+ attributeGroup() {
541
+ const result = new SnomedRefinementGroup();
542
+
543
+ this.fixed('{');
544
+ this.ws();
545
+ result.start = this.cursor;
546
+
547
+ result.refinements.push(this.attribute());
548
+ while (this.gchar(',')) {
549
+ result.refinements.push(this.attribute());
550
+ }
551
+
552
+ result.stop = this.cursor;
553
+ this.ws();
554
+ this.fixed('}');
555
+ this.ws();
556
+
557
+ return result;
558
+ }
559
+
560
+ /**
561
+ * Parse an attribute (refinement)
562
+ */
563
+ attribute() {
564
+ const result = new SnomedRefinement();
565
+ result.start = this.cursor;
566
+
567
+ result.name = this.attributeName();
568
+ this.fixed('=');
569
+ result.value = this.attributeValue();
570
+ this.ws();
571
+
572
+ result.stop = this.cursor;
573
+ return result;
574
+ }
575
+
576
+ /**
577
+ * Parse an attribute name
578
+ */
579
+ attributeName() {
580
+ const result = new SnomedConcept();
581
+ result.start = this.cursor;
582
+
583
+ this.ws();
584
+ result.code = this.conceptId();
585
+ this.ws();
586
+
587
+ if (this.gchar('|')) {
588
+ this.ws();
589
+ result.description = this.term();
590
+ this.ws();
591
+ this.fixed('|');
592
+ this.ws();
593
+ }
594
+
595
+ result.stop = this.cursor;
596
+ return result;
597
+ }
598
+
599
+ /**
600
+ * Parse an attribute value
601
+ */
602
+ attributeValue() {
603
+ this.ws();
604
+
605
+ if (this.gchar('(')) {
606
+ const result = this.expression();
607
+ this.fixed(')');
608
+ return result;
609
+ } else {
610
+ return this.expression();
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Parse a sub-expression
616
+ */
617
+ expression() {
618
+ const result = new SnomedExpression();
619
+ result.start = this.cursor;
620
+
621
+ this.ws();
622
+ result.concepts.push(this.concept());
623
+
624
+ while (this.gchar('+')) {
625
+ result.concepts.push(this.concept());
626
+ }
627
+
628
+ if (this.gchar(':')) {
629
+ this.ws();
630
+ this.refinements(result);
631
+ }
632
+
633
+ result.stop = this.cursor;
634
+ return result;
635
+ }
636
+
637
+ /**
638
+ * Parse a concept ID (sequence of digits)
639
+ */
640
+ conceptId() {
641
+ let result = '';
642
+
643
+ while (this.isDigit(this.peek())) {
644
+ result += this.next();
645
+ }
646
+
647
+ this.rule(result.length > 0, 'Concept not found (next char = "' + this.peekDisp() + '", in "' + this.source + '")');
648
+ return result;
649
+ }
650
+
651
+ /**
652
+ * Parse a decimal number
653
+ */
654
+ decimal() {
655
+ let result = '';
656
+ this.fixed('#');
657
+
658
+ while (this.isDigit(this.peek()) || this.peek() === '.') {
659
+ result += this.next();
660
+ }
661
+
662
+ return result;
663
+ }
664
+
665
+ /**
666
+ * Parse a term (text between | characters)
667
+ */
668
+ term() {
669
+ let result = '';
670
+
671
+ while (this.peek() !== '|') {
672
+ result += this.next();
673
+ }
674
+
675
+ return result;
676
+ }
677
+
678
+ /**
679
+ * Parse a string constant
680
+ */
681
+ stringConstant() {
682
+ let result = '';
683
+ this.fixed('"');
684
+
685
+ while (this.peek() !== '"' && this.peek() !== '\0') {
686
+ if (result.length > MAX_TERM_LENGTH) {
687
+ throw new Error('Constant too long (>' + MAX_TERM_LENGTH + ' chars) at character ' + this.cursor);
688
+ }
689
+ result += this.next();
690
+ }
691
+
692
+ if (this.peek() === '\0') {
693
+ throw new Error('Unterminated Constant at character ' + this.cursor);
694
+ }
695
+
696
+ this.fixed('"');
697
+ return result;
698
+ }
699
+
700
+ /**
701
+ * Skip whitespace
702
+ */
703
+ ws() {
704
+ while (this.isWhitespace(this.peek())) {
705
+ this.next();
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Try to consume a specific character
711
+ */
712
+ gchar(c) {
713
+ const result = this.peek() === c;
714
+ if (result) {
715
+ this.next();
716
+ }
717
+ return result;
718
+ }
719
+
720
+ /**
721
+ * Require a specific character
722
+ */
723
+ fixed(c) {
724
+ const success = this.gchar(c);
725
+ this.rule(success, 'Expected character "' + c + '" but found ' + this.peek());
726
+ this.ws();
727
+ }
728
+
729
+ /**
730
+ * Require a three-character prefix
731
+ */
732
+ prefix(c) {
733
+ this.fixed(c);
734
+ this.fixed(c);
735
+ this.fixed(c);
736
+ this.ws();
737
+ }
738
+
739
+ /**
740
+ * Get the next character and advance cursor
741
+ */
742
+ next() {
743
+ const result = this.peek();
744
+ this.cursor++;
745
+ return result;
746
+ }
747
+
748
+ /**
749
+ * Peek at the current character
750
+ */
751
+ peek() {
752
+ if (this.cursor >= this.source.length) {
753
+ return '\0';
754
+ }
755
+ return this.source[this.cursor];
756
+ }
757
+
758
+ /**
759
+ * Get a display string for the current character
760
+ */
761
+ peekDisp() {
762
+ if (this.cursor >= this.source.length) {
763
+ return '[n/a: overrun]';
764
+ }
765
+ return this.source[this.cursor];
766
+ }
767
+
768
+ /**
769
+ * Assert a rule and throw if it fails
770
+ */
771
+ rule(test, message) {
772
+ if (!test) {
773
+ throw new Error(message + ' at character ' + this.cursor);
774
+ }
775
+ }
776
+
777
+ /**
778
+ * Check if character is a digit
779
+ */
780
+ isDigit(c) {
781
+ return c >= '0' && c <= '9';
782
+ }
783
+
784
+ /**
785
+ * Check if character is whitespace
786
+ */
787
+ isWhitespace(c) {
788
+ return c === ' ' || c === '\t' || c === '\n' || c === '\r';
789
+ }
790
+ }
791
+
792
+ // Render options for expressions
793
+ const SnomedServicesRenderOption = {
794
+ Minimal: 0,
795
+ AsIs: 1,
796
+ FillMissing: 2,
797
+ ReplaceAll: 3
798
+ };
799
+
800
+ // Refinement group match states
801
+ const SnomedRefinementGroupMatchState = {
802
+ NoMatch: 0,
803
+ Identical: 1,
804
+ Subsumed: 2
805
+ };
806
+
807
+ /**
808
+ * Represents a matching concept with optional unmatched refinement groups
809
+ */
810
+ class MatchingConcept {
811
+ constructor(code, unmatchedGroups = null) {
812
+ this.code = code;
813
+ this.unmatchedGroups = unmatchedGroups || [];
814
+ }
815
+ }
816
+
817
+ /**
818
+ * SNOMED CT Expression Services
819
+ * Provides comprehensive expression processing capabilities
820
+ */
821
+ class SnomedExpressionServices {
822
+ constructor(snomedStructures, isAIndex) {
823
+ this.strings = snomedStructures.strings;
824
+ this.words = snomedStructures.words;
825
+ this.stems = snomedStructures.stems;
826
+ this.refs = snomedStructures.refs;
827
+ this.descriptions = snomedStructures.descriptions;
828
+ this.descriptionIndex = snomedStructures.descriptionIndex;
829
+ this.concepts = snomedStructures.concepts;
830
+ this.relationships = snomedStructures.relationships;
831
+ this.refSetMembers = snomedStructures.refSetMembers;
832
+ this.refSetIndex = snomedStructures.refSetIndex;
833
+
834
+ this.isAIndex = isAIndex;
835
+ this.defaultLanguage = 1; // Default to English
836
+ this.building = false; // Set to true during import
837
+ this.assumeClassified = true; // Optimization flag
838
+ }
839
+
840
+ /**
841
+ * Condense an expression to find matching concepts
842
+ */
843
+ condenseExpression(exp) {
844
+ const grps = [];
845
+
846
+ // Add all refinement groups
847
+ grps.push(...exp.refinementGroups);
848
+
849
+ // Convert individual refinements to single-refinement groups
850
+ for (const ref of exp.refinements) {
851
+ const grp = new SnomedRefinementGroup();
852
+ grp.refinements.push(ref);
853
+ grps.push(grp);
854
+ }
855
+
856
+ const result = [];
857
+
858
+ if (exp.concepts.length === 1) {
859
+ if (grps.length === 0) {
860
+ result.push(new MatchingConcept(exp.concepts[0].code));
861
+ } else {
862
+ this.findMatchingConcepts(result, exp.concepts[0].reference, grps);
863
+ }
864
+ }
865
+
866
+ if (result.length === 0) {
867
+ throw new Error(`No matches found for ${exp.describe()}`);
868
+ }
869
+
870
+ return result;
871
+ }
872
+
873
+ /**
874
+ * Get defining relationships for a concept
875
+ */
876
+ getDefiningRelationships(conceptIndex) {
877
+ const concept = this.concepts.getConcept(conceptIndex);
878
+ const outboundIndex = concept.outbounds;
879
+
880
+ if (outboundIndex === 0) {
881
+ return [];
882
+ }
883
+
884
+ const result = [];
885
+ const outbounds = this.refs.getReferences(outboundIndex);
886
+
887
+ for (const relIndex of outbounds) {
888
+ const rel = this.relationships.getRelationship(relIndex);
889
+
890
+ // Only include active defining relationships that are not is-a
891
+ if (rel.active && rel.defining && rel.relType !== this.isAIndex) {
892
+ result.push(relIndex);
893
+ }
894
+ }
895
+
896
+ return result;
897
+ }
898
+
899
+ /**
900
+ * Check if groups match exactly
901
+ */
902
+ groupsMatch(a, b) {
903
+ for (const refA of a.refinements) {
904
+ let refB = null;
905
+
906
+ // Find matching refinement by name
907
+ for (const testRef of b.refinements) {
908
+ if (refA.name.matches(testRef.name)) {
909
+ refB = testRef;
910
+ break;
911
+ }
912
+ }
913
+
914
+ if (!refB) {
915
+ return false;
916
+ }
917
+
918
+ // Check if values are equivalent
919
+ if (!this.expressionsEquivalent(refA.value, refB.value)) {
920
+ return false;
921
+ }
922
+ }
923
+
924
+ return true;
925
+ }
926
+
927
+ /**
928
+ * Parse expression string
929
+ */
930
+ parseExpression(source) {
931
+ const parser = new SnomedExpressionParser();
932
+ const result = parser.parse(source);
933
+ this.checkExpression(result);
934
+ return result;
935
+ }
936
+
937
+ /**
938
+ * Check if two expressions are equivalent
939
+ */
940
+ expressionsEquivalent(a, b) {
941
+ const e1 = a.canonical();
942
+ const e2 = b.canonical();
943
+
944
+ const match = e1.matches(e2);
945
+ return match === '';
946
+ }
947
+
948
+ /**
949
+ * Create defined expression from concept
950
+ */
951
+ createDefinedExpression(reference, exp, ancestor = false) {
952
+ if (this.isPrimitive(reference)) {
953
+ if (!exp.hasConcept(reference)) {
954
+ const concept = new SnomedConcept(reference);
955
+ concept.code = this.getConceptId(reference);
956
+ exp.concepts.push(concept);
957
+ }
958
+ } else {
959
+ // Add parent concepts
960
+ const parents = this.getConceptParents(reference);
961
+ for (const parent of parents) {
962
+ this.createDefinedExpression(parent, exp, true);
963
+ }
964
+
965
+ if (!ancestor || !this.assumeClassified) {
966
+ const groups = new Map(); // Group number -> refinement group
967
+
968
+ // Process defining relationships
969
+ const definingRels = this.getDefiningRelationships(reference);
970
+ for (const relIndex of definingRels) {
971
+ const rel = this.relationships.getRelationship(relIndex);
972
+
973
+ const ref = new SnomedRefinement();
974
+ ref.name = new SnomedConcept(rel.relType);
975
+ ref.name.code = this.getConceptId(rel.relType);
976
+
977
+ ref.value = new SnomedExpression();
978
+ const targetConcept = new SnomedConcept(rel.target);
979
+ targetConcept.code = this.getConceptId(rel.target);
980
+ ref.value.concepts.push(targetConcept);
981
+
982
+ if (rel.group === 0) {
983
+ // Ungrouped refinement
984
+ if (!exp.hasRefinement(ref)) {
985
+ exp.refinements.push(ref);
986
+ }
987
+ } else {
988
+ // Grouped refinement
989
+ const groupKey = rel.group.toString();
990
+ if (!groups.has(groupKey)) {
991
+ groups.set(groupKey, new SnomedRefinementGroup());
992
+ }
993
+ groups.get(groupKey).refinements.push(ref);
994
+ }
995
+ }
996
+
997
+ // Add groups to expression
998
+ for (const grp of groups.values()) {
999
+ if (!exp.hasRefinementGroup(grp)) {
1000
+ exp.refinementGroups.push(grp);
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ /**
1008
+ * Create normal form for a concept
1009
+ */
1010
+ createNormalForm(reference) {
1011
+ if (this.building) {
1012
+ const exp = new SnomedExpression();
1013
+ this.createDefinedExpression(reference, exp, false);
1014
+ return this.normaliseExpression(exp);
1015
+ } else {
1016
+ // Read from stored normal form
1017
+ const normalFormIndex = this.concepts.getNormalForm(reference);
1018
+ let source;
1019
+
1020
+ if (normalFormIndex === 0) {
1021
+ source = this.getConceptId(reference);
1022
+ } else {
1023
+ source = this.strings.getEntry(normalFormIndex);
1024
+ }
1025
+
1026
+ const result = new SnomedExpressionParser().parse(source);
1027
+ this.checkExpression(result);
1028
+ return result;
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * Rationalize expression by merging concepts and refinements
1034
+ */
1035
+ rationaliseExpression(exp) {
1036
+ // Merge subsumable concepts
1037
+ let i = 0;
1038
+ while (i < exp.concepts.length) {
1039
+ const c1 = exp.concepts[i];
1040
+ let j = i + 1;
1041
+
1042
+ while (j < exp.concepts.length) {
1043
+ const c2 = exp.concepts[j];
1044
+
1045
+ if (c1.reference !== NO_REFERENCE && c2.reference !== NO_REFERENCE) {
1046
+ if (this.subsumes(c1.reference, c2.reference)) {
1047
+ c1.copyFrom(c2);
1048
+ exp.concepts.splice(j, 1);
1049
+ } else if (this.subsumes(c2.reference, c1.reference)) {
1050
+ exp.concepts.splice(j, 1);
1051
+ } else {
1052
+ j++;
1053
+ }
1054
+ } else {
1055
+ j++;
1056
+ }
1057
+ }
1058
+ i++;
1059
+ }
1060
+
1061
+ // Merge refinements
1062
+ this.mergeRefinements(exp.refinements);
1063
+ for (const group of exp.refinementGroups) {
1064
+ this.mergeRefinements(group.refinements);
1065
+ }
1066
+
1067
+ // Merge refinement groups
1068
+ i = 0;
1069
+ while (i < exp.refinementGroups.length) {
1070
+ const grp1 = exp.refinementGroups[i];
1071
+ let j = i + 1;
1072
+
1073
+ while (j < exp.refinementGroups.length) {
1074
+ const grp2 = exp.refinementGroups[j];
1075
+
1076
+ if (this.mergeGroups(grp1, grp2)) {
1077
+ exp.refinementGroups.splice(j, 1);
1078
+ } else {
1079
+ j++;
1080
+ }
1081
+ }
1082
+ i++;
1083
+ }
1084
+ }
1085
+
1086
+ /**
1087
+ * Merge refinement groups if possible
1088
+ */
1089
+ mergeGroups(grp1, grp2) {
1090
+ // Find matching attribute names
1091
+ const matches = [];
1092
+ const targets = [];
1093
+
1094
+ for (const ref1 of grp1.refinements) {
1095
+ for (const ref2 of grp2.refinements) {
1096
+ if (ref1.name.reference === ref2.name.reference) {
1097
+ matches.push(ref1.name.reference);
1098
+ break;
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ if (matches.length === 0) {
1104
+ return false;
1105
+ }
1106
+
1107
+ // Check subsumption for each match
1108
+ let canMerge = true;
1109
+ for (const nameRef of matches) {
1110
+ const ref1 = this.getRefinementByName(nameRef, grp1.refinements);
1111
+ const ref2 = this.getRefinementByName(nameRef, grp2.refinements);
1112
+
1113
+ if (!ref1 || !ref2) {
1114
+ canMerge = false;
1115
+ break;
1116
+ }
1117
+
1118
+ if (this.expressionSubsumes(ref1.value, ref2.value)) {
1119
+ targets.push(true);
1120
+ } else if (this.expressionSubsumes(ref2.value, ref1.value)) {
1121
+ targets.push(false);
1122
+ } else {
1123
+ canMerge = false;
1124
+ break;
1125
+ }
1126
+ }
1127
+
1128
+ if (canMerge) {
1129
+ // Perform the merge
1130
+ for (let i = 0; i < matches.length; i++) {
1131
+ const nameRef = matches[i];
1132
+ if (targets[i]) {
1133
+ const ref1 = this.getRefinementByName(nameRef, grp1.refinements);
1134
+ const ref2 = this.getRefinementByName(nameRef, grp2.refinements);
1135
+ ref1.value = ref2.value;
1136
+ }
1137
+ }
1138
+
1139
+ // Add non-matching refinements from grp2 to grp1
1140
+ for (const ref2 of grp2.refinements) {
1141
+ if (!matches.includes(ref2.name.reference)) {
1142
+ grp1.refinements.push(ref2);
1143
+ }
1144
+ }
1145
+ }
1146
+
1147
+ return canMerge;
1148
+ }
1149
+
1150
+ /**
1151
+ * Get refinement by name reference
1152
+ */
1153
+ getRefinementByName(nameRef, refinements) {
1154
+ for (const ref of refinements) {
1155
+ if (ref.name.reference === nameRef) {
1156
+ return ref;
1157
+ }
1158
+ }
1159
+ return null;
1160
+ }
1161
+
1162
+ /**
1163
+ * Merge refinements in a list
1164
+ */
1165
+ mergeRefinements(list) {
1166
+ let i = 0;
1167
+ while (i < list.length) {
1168
+ const ref1 = list[i];
1169
+ let j = i + 1;
1170
+
1171
+ while (j < list.length) {
1172
+ const ref2 = list[j];
1173
+
1174
+ if (ref1.name.matches(ref2.name)) {
1175
+ if (this.expressionSubsumes(ref1.value, ref2.value)) {
1176
+ ref1.value = ref2.value;
1177
+ list.splice(j, 1);
1178
+ } else if (this.expressionSubsumes(ref2.value, ref1.value)) {
1179
+ list.splice(j, 1);
1180
+ } else {
1181
+ j++;
1182
+ }
1183
+ } else {
1184
+ j++;
1185
+ }
1186
+ }
1187
+ i++;
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * Normalize expression to normal form
1193
+ */
1194
+ normaliseExpression(exp) {
1195
+ const work = new SnomedExpression();
1196
+
1197
+ // Process concepts
1198
+ for (const concept of exp.concepts) {
1199
+ if (concept.reference === NO_REFERENCE || this.isPrimitive(concept.reference)) {
1200
+ work.concepts.push(concept);
1201
+ } else {
1202
+ const ex = this.createNormalForm(concept.reference);
1203
+ work.merge(ex);
1204
+ }
1205
+ }
1206
+
1207
+ // Process refinements
1208
+ for (const refSrc of exp.refinements) {
1209
+ const refDst = new SnomedRefinement();
1210
+ work.refinements.push(refDst);
1211
+ refDst.name = refSrc.name;
1212
+ refDst.value = this.normaliseExpression(refSrc.value);
1213
+ }
1214
+
1215
+ // Process refinement groups
1216
+ for (const grpSrc of exp.refinementGroups) {
1217
+ const grpDst = new SnomedRefinementGroup();
1218
+ work.refinementGroups.push(grpDst);
1219
+
1220
+ for (const refSrc of grpSrc.refinements) {
1221
+ const refDst = new SnomedRefinement();
1222
+ grpDst.refinements.push(refDst);
1223
+ refDst.name = refSrc.name;
1224
+ refDst.value = this.normaliseExpression(refSrc.value);
1225
+ }
1226
+ }
1227
+
1228
+ const work2 = work.canonical();
1229
+ this.rationaliseExpression(work2);
1230
+ return work2.canonical();
1231
+ }
1232
+
1233
+ /**
1234
+ * Check if expression a subsumes expression b
1235
+ */
1236
+ expressionSubsumes(a, b) {
1237
+ if (a.isSimple() && b.isSimple()) {
1238
+ return this.subsumes(a.concepts[0].reference, b.concepts[0].reference);
1239
+ }
1240
+
1241
+ const e1 = this.normaliseExpression(a);
1242
+ const e2 = this.normaliseExpression(b);
1243
+
1244
+ // Check root concepts
1245
+ for (const c of e1.concepts) {
1246
+ let ok = false;
1247
+ for (const ct of e2.concepts) {
1248
+ if (this.subsumesConcept(c, ct)) {
1249
+ ok = true;
1250
+ break;
1251
+ }
1252
+ }
1253
+ if (!ok) {
1254
+ return false;
1255
+ }
1256
+ }
1257
+
1258
+ // Check refinement groups
1259
+ for (const r of e1.refinementGroups) {
1260
+ const rt = this.findMatchingGroup(r, e2);
1261
+ if (!rt || !this.subsumesGroup(r, rt)) {
1262
+ return false;
1263
+ }
1264
+ }
1265
+
1266
+ return true;
1267
+ }
1268
+
1269
+ /**
1270
+ * Check if concept a subsumes concept b
1271
+ */
1272
+ subsumesConcept(a, b) {
1273
+ if (a.matches(b)) {
1274
+ return true;
1275
+ }
1276
+
1277
+ return (a.reference !== NO_REFERENCE) &&
1278
+ (b.reference !== NO_REFERENCE) &&
1279
+ this.subsumes(a.reference, b.reference);
1280
+ }
1281
+
1282
+ /**
1283
+ * Check if group a subsumes group b
1284
+ */
1285
+ subsumesGroup(a, b) {
1286
+ for (const refA of a.refinements) {
1287
+ let refB = null;
1288
+
1289
+ for (const testRef of b.refinements) {
1290
+ if (refA.name.matches(testRef.name)) {
1291
+ refB = testRef;
1292
+ break;
1293
+ }
1294
+ }
1295
+
1296
+ if (!refB) {
1297
+ return false;
1298
+ }
1299
+
1300
+ if (!this.expressionSubsumes(refA.value, refB.value)) {
1301
+ return false;
1302
+ }
1303
+ }
1304
+
1305
+ return true;
1306
+ }
1307
+
1308
+ /**
1309
+ * Find matching group in expression
1310
+ */
1311
+ findMatchingGroup(r, exp) {
1312
+ for (const t of exp.refinementGroups) {
1313
+ let all = true;
1314
+
1315
+ for (const refs of r.refinements) {
1316
+ let match = false;
1317
+ for (const reft of t.refinements) {
1318
+ if (refs.name.matches(reft.name)) {
1319
+ match = true;
1320
+ break;
1321
+ }
1322
+ }
1323
+ if (!match) {
1324
+ all = false;
1325
+ break;
1326
+ }
1327
+ }
1328
+
1329
+ if (all) {
1330
+ return t;
1331
+ }
1332
+ }
1333
+
1334
+ return null;
1335
+ }
1336
+
1337
+ /**
1338
+ * Find concepts matching refinement patterns
1339
+ */
1340
+ findMatchingConcepts(list, reference, refinements) {
1341
+ const children = this.getConceptChildren(reference);
1342
+
1343
+ for (const child of children) {
1344
+ const conceptId = this.getConceptId(child);
1345
+ const exp = new SnomedExpression();
1346
+ this.createDefinedExpression(child, exp, false);
1347
+
1348
+ // Convert ungrouped refinements to groups
1349
+ for (const ref of exp.refinements) {
1350
+ const grp = new SnomedRefinementGroup();
1351
+ exp.refinementGroups.push(grp);
1352
+ grp.refinements.push(ref);
1353
+ }
1354
+ exp.refinements.length = 0;
1355
+
1356
+ let allMatched = true;
1357
+ let oneUnMatched = false;
1358
+ const matchedGroups = [];
1359
+
1360
+ for (const grp of exp.refinementGroups) {
1361
+ const state = this.checkGroupStateInRefinements(grp, refinements, matchedGroups);
1362
+ if (state === SnomedRefinementGroupMatchState.NoMatch) {
1363
+ oneUnMatched = true;
1364
+ } else if (state !== SnomedRefinementGroupMatchState.Identical) {
1365
+ allMatched = false;
1366
+ }
1367
+ }
1368
+
1369
+ if (oneUnMatched) {
1370
+ // Neither this nor children will match
1371
+ continue;
1372
+ } else if (allMatched && matchedGroups.length > 0) {
1373
+ // Complete match
1374
+ list.push(new MatchingConcept(conceptId));
1375
+ } else {
1376
+ // Partial match - continue searching
1377
+ const nonMatchingGroups = this.listNonMatchingGroups(refinements, matchedGroups);
1378
+ if (nonMatchingGroups.length < refinements.length) {
1379
+ list.push(new MatchingConcept(conceptId, nonMatchingGroups));
1380
+ }
1381
+ this.findMatchingConcepts(list, child, nonMatchingGroups);
1382
+ }
1383
+ }
1384
+ }
1385
+
1386
+ /**
1387
+ * Check group state in refinements
1388
+ */
1389
+ checkGroupStateInRefinements(grp, refinements, matchedGroups) {
1390
+ for (const g of refinements) {
1391
+ if (this.groupsMatch(grp, g)) {
1392
+ matchedGroups.push(g);
1393
+ return SnomedRefinementGroupMatchState.Identical;
1394
+ } else if (this.subsumesGroup(grp, g)) {
1395
+ return SnomedRefinementGroupMatchState.Subsumed;
1396
+ }
1397
+ }
1398
+ return SnomedRefinementGroupMatchState.NoMatch;
1399
+ }
1400
+
1401
+ /**
1402
+ * List non-matching groups
1403
+ */
1404
+ listNonMatchingGroups(target, source) {
1405
+ const result = [];
1406
+
1407
+ for (const g of target) {
1408
+ const r = this.findMatchingGroupInList(g, source);
1409
+ if (!r) {
1410
+ result.push(g);
1411
+ }
1412
+ }
1413
+
1414
+ return result;
1415
+ }
1416
+
1417
+ /**
1418
+ * Find matching group in list
1419
+ */
1420
+ findMatchingGroupInList(r, groups) {
1421
+ for (const t of groups) {
1422
+ let all = true;
1423
+
1424
+ for (const refs of r.refinements) {
1425
+ let match = false;
1426
+ for (const reft of t.refinements) {
1427
+ if (refs.name.matches(reft.name)) {
1428
+ match = true;
1429
+ break;
1430
+ }
1431
+ }
1432
+ if (!match) {
1433
+ all = false;
1434
+ break;
1435
+ }
1436
+ }
1437
+
1438
+ if (all) {
1439
+ return t;
1440
+ }
1441
+ }
1442
+
1443
+ return null;
1444
+ }
1445
+
1446
+ /**
1447
+ * Validate expression structure and concept references
1448
+ */
1449
+ checkExpression(expression) {
1450
+ for (const concept of expression.concepts) {
1451
+ this.checkConcept(concept);
1452
+ }
1453
+
1454
+ if (expression.hasRefinements()) {
1455
+ for (const refinement of expression.refinements) {
1456
+ this.checkRefinement(refinement);
1457
+ }
1458
+ }
1459
+
1460
+ if (expression.hasRefinementGroups()) {
1461
+ for (const group of expression.refinementGroups) {
1462
+ for (const refinement of group.refinements) {
1463
+ this.checkRefinement(refinement);
1464
+ }
1465
+ }
1466
+ }
1467
+ }
1468
+
1469
+ /**
1470
+ * Validate concept reference
1471
+ */
1472
+ checkConcept(concept) {
1473
+ if (concept.code) {
1474
+ const conceptId = BigInt(concept.code);
1475
+ const result = this.concepts.findConcept(conceptId);
1476
+
1477
+ if (result.found) {
1478
+ concept.reference = result.index;
1479
+ } else if (concept.code !== '111115') { // Special case for some SNOMED extensions
1480
+ throw new Error(`Concept ${concept.code} not found`);
1481
+ }
1482
+ }
1483
+
1484
+ // Validate description if provided
1485
+ if (concept.reference !== NO_REFERENCE && concept.description) {
1486
+ const displayNames = this.listDisplayNames(concept.reference, 0);
1487
+ const normalizedDescription = this.normalizeText(concept.description);
1488
+
1489
+ let ok = false;
1490
+
1491
+ // Check if matches preferred display (first in list)
1492
+ if (displayNames.length > 0 && displayNames[0].term) {
1493
+ ok = this.normalizeText(displayNames[0].term) === normalizedDescription;
1494
+ }
1495
+
1496
+ // Check all designations if not already matched
1497
+ if (!ok) {
1498
+ for (const designation of displayNames) {
1499
+ if (this.normalizeText(designation.term) === normalizedDescription) {
1500
+ ok = true;
1501
+ break;
1502
+ }
1503
+ }
1504
+ }
1505
+
1506
+ if (!ok) {
1507
+ const validTerms = displayNames.map(d => d.term).join('", "');
1508
+ throw new Error(`Term "${concept.description}" doesn't match a defined term at position ${concept.start} (valid terms would be from this list: "${validTerms}")`);
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ /**
1514
+ * List all display names for a concept
1515
+ * Equivalent to Pascal ListDisplayNames procedure
1516
+ */
1517
+ listDisplayNames(conceptIndex, languageFilter = 0) {
1518
+ const designations = [];
1519
+
1520
+ try {
1521
+ const concept = this.concepts.getConcept(conceptIndex);
1522
+ const descriptionsRef = concept.descriptions;
1523
+
1524
+ if (descriptionsRef === 0) {
1525
+ return designations; // No descriptions available
1526
+ }
1527
+
1528
+ const descriptionIndices = this.refs.getReferences(descriptionsRef);
1529
+
1530
+ for (let i = 0; i < descriptionIndices.length; i++) {
1531
+ const descIndex = descriptionIndices[i];
1532
+ const description = this.descriptions.getDescription(descIndex);
1533
+
1534
+ // Only include active descriptions
1535
+ if (description.active) {
1536
+ // Language filtering (simplified - could be enhanced)
1537
+ if (languageFilter === 0 || description.lang === languageFilter) {
1538
+ const term = this.strings.getEntry(description.iDesc).trim();
1539
+
1540
+ designations.push({
1541
+ isPreferred: i === 0, // First description is considered preferred
1542
+ isActive: true,
1543
+ languageCode: this.codeForLanguage(description.lang),
1544
+ term: term,
1545
+ descriptionIndex: descIndex
1546
+ });
1547
+ }
1548
+ }
1549
+ }
1550
+ } catch (error) {
1551
+ // If we can't read the concept descriptions, return empty list
1552
+ this.log.warn(`Warning: Could not read descriptions for concept ${conceptIndex}: ${error.message}`);
1553
+ }
1554
+
1555
+ return designations;
1556
+ }
1557
+
1558
+ /**
1559
+ * Normalize text for comparison (equivalent to Pascal normalise function)
1560
+ */
1561
+ normalizeText(text) {
1562
+ if (!text) return '';
1563
+
1564
+ let result = '';
1565
+ let wasWhitespace = false;
1566
+
1567
+ for (const char of text) {
1568
+ if (this.isWhitespace(char)) {
1569
+ if (!wasWhitespace) {
1570
+ result += ' ';
1571
+ wasWhitespace = true;
1572
+ }
1573
+ } else {
1574
+ result += char.toLowerCase();
1575
+ wasWhitespace = false;
1576
+ }
1577
+ }
1578
+
1579
+ return result.trim();
1580
+ }
1581
+
1582
+ /**
1583
+ * Check if character is whitespace
1584
+ */
1585
+ isWhitespace(char) {
1586
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
1587
+ }
1588
+
1589
+ /**
1590
+ * Get language code from internal language index
1591
+ */
1592
+ codeForLanguage(langIndex) {
1593
+ // Simplified mapping - could be enhanced with proper language reference
1594
+ const languageMap = {
1595
+ 1: 'en-US',
1596
+ 2: 'en-GB',
1597
+ 3: 'es',
1598
+ 4: 'fr',
1599
+ 5: 'de'
1600
+ };
1601
+
1602
+ return languageMap[langIndex] || 'en-US';
1603
+ }
1604
+
1605
+ /**
1606
+ * Validate refinement
1607
+ */
1608
+ checkRefinement(refinement) {
1609
+ this.checkConcept(refinement.name);
1610
+ this.checkExpression(refinement.value);
1611
+ }
1612
+
1613
+ /**
1614
+ * Render expression as string
1615
+ */
1616
+ renderExpression(source, option = SnomedServicesRenderOption.AsIs) {
1617
+ const parts = [];
1618
+ this.renderExpressionParts(parts, source, option);
1619
+ return parts.join('');
1620
+ }
1621
+
1622
+ /**
1623
+ * Helper methods that need to be implemented based on SNOMED structures
1624
+ */
1625
+
1626
+ isPrimitive(reference) {
1627
+ // Check if concept is primitive based on concept flags
1628
+ // In SNOMED CT, primitive concepts are not fully defined by their relationships
1629
+ try {
1630
+ const concept = this.concepts.getConcept(reference);
1631
+ // Bit 0 typically indicates if concept is primitive (1) or defined (0)
1632
+ // This may vary based on the specific implementation
1633
+ return (concept.flags & 1) !== 0;
1634
+ } catch (error) {
1635
+ // If we can't read the concept, assume it's primitive for safety
1636
+ this.log.warn(`Warning: Could not check primitive status for concept ${reference}: ${error.message}`);
1637
+ return true;
1638
+ }
1639
+ }
1640
+
1641
+ getConceptId(reference) {
1642
+ // Get concept ID string from reference index
1643
+ try {
1644
+ const concept = this.concepts.getConcept(reference);
1645
+ return concept.identity.toString();
1646
+ } catch (error) {
1647
+ this.log.warn(`Warning: Could not get concept ID for reference ${reference}: ${error.message}`);
1648
+ return reference.toString(); // Fall back to using the reference itself
1649
+ }
1650
+ }
1651
+
1652
+ getConceptParents(reference) {
1653
+ // Get active parent concepts (needs implementation)
1654
+ const concept = this.concepts.getConcept(reference);
1655
+ const parentsIndex = concept.parents;
1656
+
1657
+ if (parentsIndex === 0) {
1658
+ return [];
1659
+ }
1660
+
1661
+ return this.refs.getReferences(parentsIndex);
1662
+ }
1663
+
1664
+ getConceptChildren(reference) {
1665
+ // Get child concepts (needs implementation based on inbound is-a relationships)
1666
+ const children = [];
1667
+ const concept = this.concepts.getConcept(reference);
1668
+ const inboundsIndex = concept.inbounds;
1669
+
1670
+ if (inboundsIndex === 0) {
1671
+ return children;
1672
+ }
1673
+
1674
+ const inbounds = this.refs.getReferences(inboundsIndex);
1675
+
1676
+ for (const relIndex of inbounds) {
1677
+ const rel = this.relationships.getRelationship(relIndex);
1678
+
1679
+ if (rel.active && rel.defining && rel.relType === this.isAIndex) {
1680
+ children.push(rel.source);
1681
+ }
1682
+ }
1683
+
1684
+ return children;
1685
+ }
1686
+
1687
+ subsumes(a, b) {
1688
+ // Check if concept a subsumes concept b using closure/descendants
1689
+ if (a === b) {
1690
+ return true;
1691
+ }
1692
+
1693
+ try {
1694
+ // Get the closure (all descendants) for concept a
1695
+ // ?? const conceptA = this.concepts.getConcept(a);
1696
+ const closureRef = this.concepts.getAllDesc(a);
1697
+
1698
+ if (closureRef === 0 || closureRef === 0xFFFFFFFF) {
1699
+ // No closure data or magic "no children" value
1700
+ return false;
1701
+ }
1702
+
1703
+ // Get the array of descendant indices
1704
+ const descendants = this.refs.getReferences(closureRef);
1705
+
1706
+ // Check if b is in the descendants of a
1707
+ return descendants.includes(b);
1708
+ } catch (error) {
1709
+ // If we can't read closure data, fall back to simple equality check
1710
+ this.log.warn(`Warning: Could not check subsumption for ${a} -> ${b}: ${error.message}`);
1711
+ return false;
1712
+ }
1713
+ }
1714
+
1715
+ /**
1716
+ * Helper to render expression parts
1717
+ */
1718
+ renderExpressionParts(parts, expr, option) {
1719
+ // Render concepts
1720
+ for (let i = 0; i < expr.concepts.length; i++) {
1721
+ if (i > 0) parts.push('+');
1722
+ this.renderConcept(parts, expr.concepts[i], option);
1723
+ }
1724
+
1725
+ // Render refinements and groups
1726
+ if (expr.hasRefinements() || expr.hasRefinementGroups()) {
1727
+ parts.push(':');
1728
+
1729
+ // Ungrouped refinements
1730
+ if (expr.hasRefinements()) {
1731
+ for (let i = 0; i < expr.refinements.length; i++) {
1732
+ if (i > 0) parts.push(',');
1733
+ this.renderRefinement(parts, expr.refinements[i], option);
1734
+ }
1735
+ }
1736
+
1737
+ // Grouped refinements
1738
+ if (expr.hasRefinementGroups()) {
1739
+ for (let j = 0; j < expr.refinementGroups.length; j++) {
1740
+ if (j > 0) parts.push(',');
1741
+ parts.push('{');
1742
+
1743
+ for (let i = 0; i < expr.refinementGroups[j].refinements.length; i++) {
1744
+ if (i > 0) parts.push(',');
1745
+ this.renderRefinement(parts, expr.refinementGroups[j].refinements[i], option);
1746
+ }
1747
+
1748
+ parts.push('}');
1749
+ }
1750
+ }
1751
+ }
1752
+ }
1753
+
1754
+ renderConcept(parts, expr, option) {
1755
+ if (expr.reference !== NO_REFERENCE && expr.code === '') {
1756
+ expr.code = this.getConceptId(expr.reference);
1757
+ parts.push(expr.code);
1758
+ } else {
1759
+ parts.push(expr.describe());
1760
+ }
1761
+
1762
+ let description = '';
1763
+ switch (option) {
1764
+ case SnomedServicesRenderOption.Minimal:
1765
+ description = '';
1766
+ break;
1767
+ case SnomedServicesRenderOption.AsIs:
1768
+ description = expr.description;
1769
+ break;
1770
+ case SnomedServicesRenderOption.FillMissing:
1771
+ description = expr.description;
1772
+ if (description === '') {
1773
+ if (expr.reference !== NO_REFERENCE) {
1774
+ description = this.getDisplayName(expr.reference);
1775
+ } else if (expr.code) {
1776
+ description = this.getDisplayName(expr.code);
1777
+ }
1778
+ }
1779
+ break;
1780
+ case SnomedServicesRenderOption.ReplaceAll:
1781
+ if (expr.code) {
1782
+ description = this.getDisplayName(expr.code);
1783
+ }
1784
+ break;
1785
+ }
1786
+
1787
+ if (description) {
1788
+ parts.push('|');
1789
+ parts.push(description);
1790
+ parts.push('|');
1791
+ }
1792
+ }
1793
+
1794
+ renderRefinement(parts, expr, option) {
1795
+ this.renderConcept(parts, expr.name, option);
1796
+ parts.push('=');
1797
+ this.renderExpressionParts(parts, expr.value, option);
1798
+ }
1799
+
1800
+ getDisplayName(conceptIdOrReference) {
1801
+ // Get display name for concept
1802
+ let conceptIndex;
1803
+
1804
+ if (typeof conceptIdOrReference === 'string') {
1805
+ // It's a concept ID string, need to find the concept
1806
+ const conceptId = BigInt(conceptIdOrReference);
1807
+ const result = this.concepts.findConcept(conceptId);
1808
+ if (!result.found) {
1809
+ return '';
1810
+ }
1811
+ conceptIndex = result.index;
1812
+ } else {
1813
+ // It's already a concept index
1814
+ conceptIndex = conceptIdOrReference;
1815
+ }
1816
+
1817
+ // Get the preferred display name (FSN or first active description)
1818
+ const displayNames = this.listDisplayNames(conceptIndex, this.defaultLanguage);
1819
+
1820
+ if (displayNames.length > 0) {
1821
+ // Look for FSN first (if we can detect it), otherwise use first available
1822
+ for (const designation of displayNames) {
1823
+ if (designation.isPreferred) {
1824
+ return designation.term;
1825
+ }
1826
+ }
1827
+ // Fall back to first designation
1828
+ return displayNames[0].term;
1829
+ }
1830
+
1831
+ return '';
1832
+ }
1833
+ }
1834
+
1835
+ /**
1836
+ * Expression context for maintaining state during processing
1837
+ */
1838
+ class SnomedExpressionContext {
1839
+ constructor(source, expression) {
1840
+ this.source = source || '';
1841
+ this.expression = expression;
1842
+ }
1843
+
1844
+ static fromReference(reference) {
1845
+ const expression = new SnomedExpression();
1846
+ expression.concepts.push(new SnomedConcept(reference));
1847
+ return new SnomedExpressionContext('', expression);
1848
+ }
1849
+
1850
+ static fromSource(source, reference) {
1851
+ const expression = new SnomedExpression();
1852
+ expression.concepts.push(new SnomedConcept(reference));
1853
+ return new SnomedExpressionContext(source, expression);
1854
+ }
1855
+
1856
+ isComplex() {
1857
+ return this.expression.isComplex();
1858
+ }
1859
+
1860
+ getReference() {
1861
+ return this.expression.concepts[0].reference;
1862
+ }
1863
+
1864
+ setExpression(expression) {
1865
+ this.expression = expression;
1866
+ }
1867
+ }
1868
+
1869
+ module.exports = {
1870
+ SnomedExpressionServices,
1871
+ SnomedExpressionContext,
1872
+ MatchingConcept,
1873
+ SnomedServicesRenderOption,
1874
+ SnomedRefinementGroupMatchState
1875
+ };
1876
+
1877
+ // Also add this method to SnomedExpressionServices to help with debugging
1878
+ class SnomedExpressionServicesExtended extends SnomedExpressionServices {
1879
+ constructor(snomedStructures, isAIndex) {
1880
+ super(snomedStructures, isAIndex);
1881
+
1882
+ // Ensure building flag is accessible
1883
+ this.building = false;
1884
+ }
1885
+
1886
+ /**
1887
+ * Enhanced getDefiningRelationships with better error handling
1888
+ */
1889
+ getDefiningRelationships(conceptIndex) {
1890
+ try {
1891
+ const concept = this.concepts.getConcept(conceptIndex);
1892
+ const outboundIndex = concept.outbounds;
1893
+
1894
+ if (outboundIndex === 0) {
1895
+ return [];
1896
+ }
1897
+
1898
+ const result = [];
1899
+ const outbounds = this.refs.getReferences(outboundIndex);
1900
+
1901
+ if (!outbounds) {
1902
+ return [];
1903
+ }
1904
+
1905
+ for (const relIndex of outbounds) {
1906
+ try {
1907
+ const rel = this.relationships.getRelationship(relIndex);
1908
+
1909
+ // Only include active defining relationships that are not is-a
1910
+ if (rel.active && rel.defining && rel.relType !== this.isAIndex) {
1911
+ result.push(relIndex);
1912
+ }
1913
+ } catch (error) {
1914
+ // Skip problematic relationships
1915
+ if (this.building) {
1916
+ this.log.warn(`Warning: Could not read relationship ${relIndex}: ${error.message}`);
1917
+ }
1918
+ }
1919
+ }
1920
+
1921
+ return result;
1922
+ } catch (error) {
1923
+ if (this.building) {
1924
+ this.log.warn(`Warning: Could not get defining relationships for concept ${conceptIndex}: ${error.message}`);
1925
+ }
1926
+ return [];
1927
+ }
1928
+ }
1929
+
1930
+ /**
1931
+ * Enhanced getConceptParents with better error handling
1932
+ */
1933
+ getConceptParents(reference) {
1934
+ try {
1935
+ const concept = this.concepts.getConcept(reference);
1936
+ const parentsIndex = concept.parents;
1937
+
1938
+ if (parentsIndex === 0) {
1939
+ return [];
1940
+ }
1941
+
1942
+ const parents = this.refs.getReferences(parentsIndex);
1943
+ return parents || [];
1944
+ } catch (error) {
1945
+ if (this.building) {
1946
+ this.log.warn(`Warning: Could not get parents for concept ${reference}: ${error.message}`);
1947
+ }
1948
+ return [];
1949
+ }
1950
+ }
1951
+
1952
+ /**
1953
+ * Enhanced isPrimitive check with fallback
1954
+ */
1955
+ isPrimitive(reference) {
1956
+ try {
1957
+ const concept = this.concepts.getConcept(reference);
1958
+ // In SNOMED CT, primitive concepts have the primitive flag set
1959
+ // The definitionStatusId in RF2 determines this:
1960
+ // 900000000000074008 = primitive, 900000000000073002 = fully defined
1961
+ return (concept.flags & 1) !== 0;
1962
+ } catch (error) {
1963
+ // If we can't read the concept, assume it's primitive for safety
1964
+ if (this.building) {
1965
+ this.log.warn(`Warning: Could not check primitive status for concept ${reference}, assuming primitive: ${error.message}`);
1966
+ }
1967
+ return true;
1968
+ }
1969
+ }
1970
+
1971
+ /**
1972
+ * Enhanced createDefinedExpression with better error handling
1973
+ */
1974
+ createDefinedExpression(reference, exp, ancestor = false) {
1975
+ try {
1976
+ if (this.isPrimitive(reference)) {
1977
+ if (!exp.hasConcept(reference)) {
1978
+ const concept = new SnomedConcept(reference);
1979
+ concept.code = this.getConceptId(reference);
1980
+ exp.concepts.push(concept);
1981
+ }
1982
+ return;
1983
+ }
1984
+
1985
+ // Add parent concepts
1986
+ const parents = this.getConceptParents(reference);
1987
+ for (const parent of parents) {
1988
+ this.createDefinedExpression(parent, exp, true);
1989
+ }
1990
+
1991
+ if (!ancestor || !this.assumeClassified) {
1992
+ const groups = new Map(); // Group number -> refinement group
1993
+
1994
+ // Process defining relationships
1995
+ const definingRels = this.getDefiningRelationships(reference);
1996
+ for (const relIndex of definingRels) {
1997
+ try {
1998
+ const rel = this.relationships.getRelationship(relIndex);
1999
+
2000
+ const ref = new SnomedRefinement();
2001
+ ref.name = new SnomedConcept(rel.relType);
2002
+ ref.name.code = this.getConceptId(rel.relType);
2003
+
2004
+ ref.value = new SnomedExpression();
2005
+ const targetConcept = new SnomedConcept(rel.target);
2006
+ targetConcept.code = this.getConceptId(rel.target);
2007
+ ref.value.concepts.push(targetConcept);
2008
+
2009
+ if (rel.group === 0) {
2010
+ // Ungrouped refinement
2011
+ if (!exp.hasRefinement(ref)) {
2012
+ exp.refinements.push(ref);
2013
+ }
2014
+ } else {
2015
+ // Grouped refinement
2016
+ const groupKey = rel.group.toString();
2017
+ if (!groups.has(groupKey)) {
2018
+ groups.set(groupKey, new SnomedRefinementGroup());
2019
+ }
2020
+ groups.get(groupKey).refinements.push(ref);
2021
+ }
2022
+ } catch (error) {
2023
+ // Skip problematic relationships but continue
2024
+ if (this.building) {
2025
+ this.log.warn(`Warning: Could not process relationship ${relIndex}: ${error.message}`);
2026
+ }
2027
+ }
2028
+ }
2029
+
2030
+ // Add groups to expression
2031
+ for (const grp of groups.values()) {
2032
+ if (!exp.hasRefinementGroup(grp)) {
2033
+ exp.refinementGroups.push(grp);
2034
+ }
2035
+ }
2036
+ }
2037
+ } catch (error) {
2038
+ if (this.building) {
2039
+ this.log.warn(`Warning: Could not create defined expression for concept ${reference}: ${error.message}`);
2040
+ }
2041
+ // Add as primitive concept as fallback
2042
+ if (!exp.hasConcept(reference)) {
2043
+ const concept = new SnomedConcept(reference);
2044
+ concept.code = this.getConceptId(reference);
2045
+ exp.concepts.push(concept);
2046
+ }
2047
+ }
2048
+ }
2049
+ }
2050
+
2051
+
2052
+ // Export classes and constants
2053
+ module.exports = {
2054
+ // Expression status and constants
2055
+ SnomedExpressionStatus,
2056
+ MAX_TERM_LENGTH,
2057
+ NO_REFERENCE,
2058
+
2059
+ // Core expression classes
2060
+ SnomedExpressionBase,
2061
+ SnomedConcept,
2062
+ SnomedRefinement,
2063
+ SnomedRefinementGroup,
2064
+ SnomedExpression,
2065
+ SnomedExpressionParser,
2066
+
2067
+ // Services and context
2068
+ SnomedExpressionServices,
2069
+ SnomedExpressionContext,
2070
+ MatchingConcept,
2071
+ SnomedExpressionServicesExtended,
2072
+
2073
+ // Enums and constants
2074
+ SnomedServicesRenderOption,
2075
+ SnomedRefinementGroupMatchState
2076
+ };
2077
+