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
package/tx/sct/ecl.js ADDED
@@ -0,0 +1,1560 @@
1
+ /**
2
+ * SNOMED CT Expression Constraint Language (ECL) Validator
3
+ *
4
+ * This implementation provides parsing, validation, and evaluation of ECL expressions
5
+ * against SNOMED CT data using the existing SnomedServices infrastructure.
6
+ *
7
+ * Supports ECL v2.1 specification from SNOMED International
8
+ */
9
+
10
+ const { SnomedFilterContext } = require('../cs/cs-snomed');
11
+
12
+ // ECL Token Types
13
+ const ECLTokenType = {
14
+ // Literals
15
+ SCTID: 'SCTID',
16
+ TERM: 'TERM',
17
+ STRING: 'STRING',
18
+ INTEGER: 'INTEGER',
19
+ DECIMAL: 'DECIMAL',
20
+
21
+ // Operators
22
+ CHILD_OF: 'CHILD_OF', // <
23
+ CHILD_OR_SELF_OF: 'CHILD_OR_SELF_OF', // <<
24
+ DESCENDANT_OF: 'DESCENDANT_OF', // <!
25
+ DESCENDANT_OR_SELF_OF: 'DESCENDANT_OR_SELF_OF', // <<!
26
+ PARENT_OF: 'PARENT_OF', // >
27
+ PARENT_OR_SELF_OF: 'PARENT_OR_SELF_OF', // >>
28
+ ANCESTOR_OF: 'ANCESTOR_OF', // >!
29
+ ANCESTOR_OR_SELF_OF: 'ANCESTOR_OR_SELF_OF', // >>!
30
+
31
+ // Set operators
32
+ AND: 'AND',
33
+ OR: 'OR',
34
+ MINUS: 'MINUS',
35
+
36
+ // Refinement
37
+ COLON: 'COLON', // :
38
+ EQUALS: 'EQUALS', // =
39
+ NOT_EQUALS: 'NOT_EQUALS', // !=
40
+
41
+ // Comparison operators
42
+ LT: 'LT', // <
43
+ LTE: 'LTE', // <=
44
+ GT: 'GT', // >
45
+ GTE: 'GTE', // >=
46
+
47
+ // Special
48
+ MEMBER_OF: 'MEMBER_OF', // ^
49
+ WILDCARD: 'WILDCARD', // *
50
+ DOT: 'DOT', // .
51
+ CARDINALITY_RANGE: 'CARDINALITY_RANGE', // ..
52
+ REVERSE: 'REVERSE', // R
53
+
54
+ // Brackets
55
+ LPAREN: 'LPAREN', // (
56
+ RPAREN: 'RPAREN', // )
57
+ LBRACE: 'LBRACE', // {
58
+ RBRACE: 'RBRACE', // }
59
+ LBRACKET: 'LBRACKET', // [
60
+ RBRACKET: 'RBRACKET', // ]
61
+
62
+ // Separators
63
+ COMMA: 'COMMA', // ,
64
+ PIPE: 'PIPE', // |
65
+ HASH: 'HASH', // #
66
+
67
+ // Special tokens
68
+ WS: 'WS',
69
+ EOF: 'EOF',
70
+ ERROR: 'ERROR'
71
+ };
72
+
73
+ // ECL Expression Node Types
74
+ const ECLNodeType = {
75
+ SIMPLE_EXPRESSION_CONSTRAINT: 'SimpleExpressionConstraint',
76
+ REFINED_EXPRESSION_CONSTRAINT: 'RefinedExpressionConstraint',
77
+ COMPOUND_EXPRESSION_CONSTRAINT: 'CompoundExpressionConstraint',
78
+ DOTTED_EXPRESSION_CONSTRAINT: 'DottedExpressionConstraint',
79
+
80
+ CONCEPT_REFERENCE: 'ConceptReference',
81
+ WILDCARD: 'Wildcard',
82
+ MEMBER_OF: 'MemberOf',
83
+
84
+ REFINEMENT: 'Refinement',
85
+ ATTRIBUTE_SET: 'AttributeSet',
86
+ ATTRIBUTE_GROUP: 'AttributeGroup',
87
+ ATTRIBUTE: 'Attribute',
88
+
89
+ SUB_EXPRESSION_CONSTRAINT: 'SubExpressionConstraint',
90
+ EXPRESSION_COMPARISON: 'ExpressionComparison',
91
+ NUMERIC_COMPARISON: 'NumericComparison',
92
+ STRING_COMPARISON: 'StringComparison',
93
+
94
+ CONJUNCTION: 'Conjunction',
95
+ DISJUNCTION: 'Disjunction',
96
+ EXCLUSION: 'Exclusion',
97
+
98
+ CARDINALITY: 'Cardinality'
99
+ };
100
+
101
+ /**
102
+ * ECL Lexer - Tokenizes ECL expressions
103
+ */
104
+ class ECLLexer {
105
+ constructor(input) {
106
+ this.input = input;
107
+ this.position = 0;
108
+ this.current = this.input[0] || null;
109
+ }
110
+
111
+ error(message) {
112
+ throw new Error(`Lexer error at position ${this.position}: ${message}`);
113
+ }
114
+
115
+ advance() {
116
+ this.position++;
117
+ this.current = this.position < this.input.length ? this.input[this.position] : null;
118
+ }
119
+
120
+ peek(offset = 1) {
121
+ const pos = this.position + offset;
122
+ return pos < this.input.length ? this.input[pos] : null;
123
+ }
124
+
125
+ skipWhitespace() {
126
+ while (this.current && /\s/.test(this.current)) {
127
+ this.advance();
128
+ }
129
+ }
130
+
131
+ readSCTID() {
132
+ let value = '';
133
+ while (this.current && /\d/.test(this.current)) {
134
+ value += this.current;
135
+ this.advance();
136
+ }
137
+ return value;
138
+ }
139
+
140
+ readTerm() {
141
+ let value = '';
142
+ // We should be positioned at the opening |
143
+ if (this.current !== '|') {
144
+ this.error('Expected | at start of term');
145
+ }
146
+ this.advance(); // Skip opening |
147
+
148
+ while (this.current && this.current !== '|') {
149
+ value += this.current;
150
+ this.advance();
151
+ }
152
+
153
+ if (this.current !== '|') {
154
+ this.error('Unterminated term - missing closing |');
155
+ }
156
+
157
+ this.advance(); // Skip closing |
158
+ return value.trim();
159
+ }
160
+
161
+ readString() {
162
+ let value = '';
163
+ const quote = this.current;
164
+ this.advance(); // Skip opening quote
165
+
166
+ while (this.current && this.current !== quote) {
167
+ if (this.current === '\\') {
168
+ this.advance();
169
+ if (this.current) {
170
+ value += this.current;
171
+ this.advance();
172
+ }
173
+ } else {
174
+ value += this.current;
175
+ this.advance();
176
+ }
177
+ }
178
+
179
+ if (this.current !== quote) {
180
+ this.error('Unterminated string');
181
+ }
182
+
183
+ this.advance(); // Skip closing quote
184
+ return value;
185
+ }
186
+
187
+ readNumber() {
188
+ let value = '';
189
+ let hasDecimal = false;
190
+
191
+ // Handle negative numbers
192
+ if (this.current === '-') {
193
+ value += this.current;
194
+ this.advance();
195
+ }
196
+
197
+ while (this.current && (/\d/.test(this.current) || (this.current === '.' && !hasDecimal))) {
198
+ if (this.current === '.') {
199
+ hasDecimal = true;
200
+ }
201
+ value += this.current;
202
+ this.advance();
203
+ }
204
+
205
+ return {
206
+ value,
207
+ type: hasDecimal ? ECLTokenType.DECIMAL : ECLTokenType.INTEGER
208
+ };
209
+ }
210
+
211
+ getNextToken() {
212
+ while (this.current) {
213
+ if (/\s/.test(this.current)) {
214
+ this.skipWhitespace();
215
+ continue;
216
+ }
217
+
218
+ // Single character tokens
219
+ switch (this.current) {
220
+ case '(':
221
+ this.advance();
222
+ return { type: ECLTokenType.LPAREN, value: '(' };
223
+ case ')':
224
+ this.advance();
225
+ return { type: ECLTokenType.RPAREN, value: ')' };
226
+ case '{':
227
+ this.advance();
228
+ return { type: ECLTokenType.LBRACE, value: '{' };
229
+ case '}':
230
+ this.advance();
231
+ return { type: ECLTokenType.RBRACE, value: '}' };
232
+ case '[':
233
+ this.advance();
234
+ return { type: ECLTokenType.LBRACKET, value: '[' };
235
+ case ']':
236
+ this.advance();
237
+ return { type: ECLTokenType.RBRACKET, value: ']' };
238
+ case ',':
239
+ this.advance();
240
+ return { type: ECLTokenType.COMMA, value: ',' };
241
+ case '.':
242
+ // Check for cardinality range operator (..)
243
+ if (this.peek() === '.') {
244
+ this.advance();
245
+ this.advance();
246
+ return { type: ECLTokenType.CARDINALITY_RANGE, value: '..' };
247
+ } else {
248
+ this.advance();
249
+ return { type: ECLTokenType.DOT, value: '.' };
250
+ }
251
+ case ':':
252
+ this.advance();
253
+ return { type: ECLTokenType.COLON, value: ':' };
254
+ case '^':
255
+ this.advance();
256
+ return { type: ECLTokenType.MEMBER_OF, value: '^' };
257
+ case '*':
258
+ this.advance();
259
+ return { type: ECLTokenType.WILDCARD, value: '*' };
260
+ case '#':
261
+ this.advance();
262
+ return { type: ECLTokenType.HASH, value: '#' };
263
+ case '|': {
264
+ // This is a term - read the entire |term| construct
265
+ const termValue = this.readTerm();
266
+ return {type: ECLTokenType.TERM, value: termValue};
267
+ }
268
+ }
269
+
270
+ // Multi-character operators
271
+ if (this.current === '<') {
272
+ if (this.peek() === '<') {
273
+ if (this.peek(2) === '!') {
274
+ this.advance();
275
+ this.advance();
276
+ this.advance();
277
+ return { type: ECLTokenType.DESCENDANT_OR_SELF_OF, value: '<<!', };
278
+ } else {
279
+ this.advance();
280
+ this.advance();
281
+ return { type: ECLTokenType.CHILD_OR_SELF_OF, value: '<<' };
282
+ }
283
+ } else if (this.peek() === '!') {
284
+ this.advance();
285
+ this.advance();
286
+ return { type: ECLTokenType.DESCENDANT_OF, value: '<!' };
287
+ } else if (this.peek() === '=') {
288
+ this.advance();
289
+ this.advance();
290
+ return { type: ECLTokenType.LTE, value: '<=' };
291
+ } else {
292
+ this.advance();
293
+ return { type: ECLTokenType.CHILD_OF, value: '<' };
294
+ }
295
+ }
296
+
297
+ if (this.current === '>') {
298
+ if (this.peek() === '>') {
299
+ if (this.peek(2) === '!') {
300
+ this.advance();
301
+ this.advance();
302
+ this.advance();
303
+ return { type: ECLTokenType.ANCESTOR_OR_SELF_OF, value: '>>!' };
304
+ } else {
305
+ this.advance();
306
+ this.advance();
307
+ return { type: ECLTokenType.PARENT_OR_SELF_OF, value: '>>' };
308
+ }
309
+ } else if (this.peek() === '!') {
310
+ this.advance();
311
+ this.advance();
312
+ return { type: ECLTokenType.ANCESTOR_OF, value: '>!' };
313
+ } else if (this.peek() === '=') {
314
+ this.advance();
315
+ this.advance();
316
+ return { type: ECLTokenType.GTE, value: '>=' };
317
+ } else {
318
+ this.advance();
319
+ return { type: ECLTokenType.PARENT_OF, value: '>' };
320
+ }
321
+ }
322
+
323
+ if (this.current === '=') {
324
+ this.advance();
325
+ return { type: ECLTokenType.EQUALS, value: '=' };
326
+ }
327
+
328
+ if (this.current === '!') {
329
+ if (this.peek() === '=') {
330
+ this.advance();
331
+ this.advance();
332
+ return { type: ECLTokenType.NOT_EQUALS, value: '!=' };
333
+ } else {
334
+ this.error(`Unexpected character: ${this.current}`);
335
+ }
336
+ }
337
+
338
+ // String literals
339
+ if (this.current === '"' || this.current === "'") {
340
+ return { type: ECLTokenType.STRING, value: this.readString() };
341
+ }
342
+
343
+ // Handle numbers - check for decimal first, then SCTID
344
+ if (/\d/.test(this.current)) {
345
+ // Look ahead to see if this is a decimal number (digit.digit with no space)
346
+ let pos = this.position;
347
+ while (pos < this.input.length && /\d/.test(this.input[pos])) {
348
+ pos++;
349
+ }
350
+
351
+ // Check if immediately followed by .digit (decimal number)
352
+ if (pos < this.input.length &&
353
+ this.input[pos] === '.' &&
354
+ pos + 1 < this.input.length &&
355
+ /\d/.test(this.input[pos + 1])) {
356
+ // This is a decimal number - parse it completely
357
+ const num = this.readNumber();
358
+ return { type: num.type, value: num.value };
359
+ } else {
360
+ // This is a SCTID (concept ID)
361
+ const value = this.readSCTID();
362
+ return { type: ECLTokenType.SCTID, value };
363
+ }
364
+ }
365
+
366
+ // Handle negative numbers separately
367
+ if (this.current === '-' && /\d/.test(this.peek())) {
368
+ const num = this.readNumber();
369
+ return { type: num.type, value: num.value };
370
+ }
371
+
372
+ // Keywords and identifiers
373
+ if (/[a-zA-Z_]/.test(this.current)) {
374
+ let value = '';
375
+ while (this.current && /[a-zA-Z0-9_]/.test(this.current)) {
376
+ value += this.current;
377
+ this.advance();
378
+ }
379
+
380
+ // Check for keywords
381
+ switch (value.toUpperCase()) {
382
+ case 'AND':
383
+ return { type: ECLTokenType.AND, value: value };
384
+ case 'OR':
385
+ return { type: ECLTokenType.OR, value: value };
386
+ case 'MINUS':
387
+ return { type: ECLTokenType.MINUS, value: value };
388
+ case 'R':
389
+ return { type: ECLTokenType.REVERSE, value: value };
390
+ default:
391
+ // Could be a namespace identifier or other construct
392
+ return { type: ECLTokenType.SCTID, value: value };
393
+ }
394
+ }
395
+
396
+ this.error(`Unexpected character: ${this.current}`);
397
+ }
398
+
399
+ return { type: ECLTokenType.EOF, value: null };
400
+ }
401
+
402
+ tokenize() {
403
+ const tokens = [];
404
+ let token;
405
+
406
+ do {
407
+ token = this.getNextToken();
408
+ tokens.push(token);
409
+ } while (token.type !== ECLTokenType.EOF);
410
+
411
+ return tokens;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * ECL Parser - Parses tokens into AST
417
+ */
418
+ class ECLParser {
419
+ constructor(tokens) {
420
+ this.tokens = tokens;
421
+ this.position = 0;
422
+ this.current = this.tokens[0] || { type: ECLTokenType.EOF };
423
+ }
424
+
425
+ error(message) {
426
+ throw new Error(`Parser error at token ${this.position}: ${message}. Current token: ${this.current.type}(${this.current.value})`);
427
+ }
428
+
429
+ advance() {
430
+ this.position++;
431
+ this.current = this.position < this.tokens.length ? this.tokens[this.position] : { type: ECLTokenType.EOF };
432
+ }
433
+
434
+ peek(offset = 1) {
435
+ const pos = this.position + offset;
436
+ return pos < this.tokens.length ? this.tokens[pos] : { type: ECLTokenType.EOF };
437
+ }
438
+
439
+ expect(tokenType) {
440
+ if (this.current.type !== tokenType) {
441
+ this.error(`Expected ${tokenType}, got ${this.current.type}`);
442
+ }
443
+ const token = this.current;
444
+ this.advance();
445
+ return token;
446
+ }
447
+
448
+ match(...tokenTypes) {
449
+ return tokenTypes.includes(this.current.type);
450
+ }
451
+
452
+ // Main parsing entry point
453
+ parse() {
454
+ const result = this.parseExpressionConstraint();
455
+ if (this.current.type !== ECLTokenType.EOF) {
456
+ this.error('Unexpected tokens after end of expression');
457
+ }
458
+ return result;
459
+ }
460
+
461
+ parseExpressionConstraint() {
462
+ return this.parseCompoundExpressionConstraint();
463
+ }
464
+
465
+ parseCompoundExpressionConstraint() {
466
+ let left = this.parseRefinedExpressionConstraint();
467
+
468
+ while (this.match(ECLTokenType.AND, ECLTokenType.OR, ECLTokenType.MINUS)) {
469
+ const operator = this.current;
470
+ this.advance();
471
+ const right = this.parseRefinedExpressionConstraint();
472
+
473
+ const nodeType = operator.type === ECLTokenType.AND ? ECLNodeType.CONJUNCTION :
474
+ operator.type === ECLTokenType.OR ? ECLNodeType.DISJUNCTION :
475
+ ECLNodeType.EXCLUSION;
476
+
477
+ left = {
478
+ type: ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT,
479
+ operator: nodeType,
480
+ left,
481
+ right
482
+ };
483
+ }
484
+
485
+ return left;
486
+ }
487
+
488
+ parseRefinedExpressionConstraint() {
489
+ let base = this.parseDottedExpressionConstraint();
490
+
491
+ if (this.match(ECLTokenType.COLON)) {
492
+ this.advance(); // consume :
493
+ const refinement = this.parseRefinement();
494
+
495
+ return {
496
+ type: ECLNodeType.REFINED_EXPRESSION_CONSTRAINT,
497
+ base,
498
+ refinement
499
+ };
500
+ }
501
+
502
+ return base;
503
+ }
504
+
505
+ parseDottedExpressionConstraint() {
506
+ let base = this.parseSubExpressionConstraint();
507
+
508
+ const attributes = [];
509
+ while (this.match(ECLTokenType.DOT)) {
510
+ this.advance(); // consume .
511
+ const attribute = this.parseAttributeName();
512
+ attributes.push(attribute);
513
+ }
514
+
515
+ if (attributes.length > 0) {
516
+ return {
517
+ type: ECLNodeType.DOTTED_EXPRESSION_CONSTRAINT,
518
+ base,
519
+ attributes
520
+ };
521
+ }
522
+
523
+ return base;
524
+ }
525
+
526
+ parseSubExpressionConstraint() {
527
+ // Handle constraint operators
528
+ let operator = null;
529
+ if (this.match(
530
+ ECLTokenType.CHILD_OF, ECLTokenType.CHILD_OR_SELF_OF,
531
+ ECLTokenType.DESCENDANT_OF, ECLTokenType.DESCENDANT_OR_SELF_OF,
532
+ ECLTokenType.PARENT_OF, ECLTokenType.PARENT_OR_SELF_OF,
533
+ ECLTokenType.ANCESTOR_OF, ECLTokenType.ANCESTOR_OR_SELF_OF
534
+ )) {
535
+ operator = this.current;
536
+ this.advance();
537
+ }
538
+
539
+ let focus;
540
+
541
+ // Handle memberOf
542
+ if (this.match(ECLTokenType.MEMBER_OF)) {
543
+ this.advance(); // consume ^
544
+ // Parse the reference set - can be concept reference, wildcard, or parenthesized expression
545
+ // but NOT another constraint operator or member-of expression
546
+ let refSet;
547
+ if (this.match(ECLTokenType.LPAREN)) {
548
+ this.advance(); // consume (
549
+ refSet = this.parseExpressionConstraint();
550
+ this.expect(ECLTokenType.RPAREN);
551
+ } else {
552
+ refSet = this.parseEclFocusConcept();
553
+ }
554
+
555
+ focus = {
556
+ type: ECLNodeType.MEMBER_OF,
557
+ refSet
558
+ };
559
+ } else if (this.match(ECLTokenType.LPAREN)) {
560
+ this.advance(); // consume (
561
+ focus = this.parseExpressionConstraint();
562
+ this.expect(ECLTokenType.RPAREN);
563
+ } else {
564
+ focus = this.parseEclFocusConcept();
565
+ }
566
+
567
+ const result = {
568
+ type: ECLNodeType.SUB_EXPRESSION_CONSTRAINT,
569
+ operator: operator ? operator.type : null,
570
+ focus
571
+ };
572
+
573
+ return result;
574
+ }
575
+
576
+ parseEclFocusConcept() {
577
+ if (this.match(ECLTokenType.WILDCARD)) {
578
+ this.advance();
579
+ return {
580
+ type: ECLNodeType.WILDCARD
581
+ };
582
+ }
583
+
584
+ if (this.match(ECLTokenType.SCTID)) {
585
+ const conceptId = this.current.value;
586
+ this.advance();
587
+
588
+ let term = null;
589
+ if (this.match(ECLTokenType.TERM)) {
590
+ term = this.current.value;
591
+ this.advance();
592
+ }
593
+
594
+ return {
595
+ type: ECLNodeType.CONCEPT_REFERENCE,
596
+ conceptId,
597
+ term
598
+ };
599
+ }
600
+
601
+ this.error('Expected concept reference or wildcard');
602
+ }
603
+
604
+ parseRefinement() {
605
+ return this.parseAttributeSet();
606
+ }
607
+
608
+ parseAttributeSet() {
609
+ const attributes = [];
610
+
611
+ do {
612
+ if (this.match(ECLTokenType.LBRACE)) {
613
+ // Attribute group
614
+ attributes.push(this.parseAttributeGroup());
615
+ } else {
616
+ // Single attribute
617
+ attributes.push(this.parseAttribute());
618
+ }
619
+ } while (this.match(ECLTokenType.COMMA) && (this.advance(), true));
620
+
621
+ if (attributes.length === 1) {
622
+ return attributes[0];
623
+ }
624
+
625
+ return {
626
+ type: ECLNodeType.ATTRIBUTE_SET,
627
+ attributes
628
+ };
629
+ }
630
+
631
+ parseAttributeGroup() {
632
+ let cardinality = null;
633
+
634
+ // Check for cardinality before {
635
+ if (this.match(ECLTokenType.LBRACKET)) {
636
+ cardinality = this.parseCardinality();
637
+ }
638
+
639
+ this.expect(ECLTokenType.LBRACE);
640
+
641
+ const attributes = [];
642
+ do {
643
+ attributes.push(this.parseAttribute());
644
+ } while (this.match(ECLTokenType.COMMA) && (this.advance(), true));
645
+
646
+ this.expect(ECLTokenType.RBRACE);
647
+
648
+ return {
649
+ type: ECLNodeType.ATTRIBUTE_GROUP,
650
+ cardinality,
651
+ attributes
652
+ };
653
+ }
654
+
655
+ parseAttribute() {
656
+ let cardinality = null;
657
+ let reverse = false;
658
+
659
+ // Check for cardinality first - this must come before attribute name
660
+ if (this.match(ECLTokenType.LBRACKET)) {
661
+ cardinality = this.parseCardinality();
662
+ }
663
+
664
+ // Check for reverse flag
665
+ if (this.match(ECLTokenType.REVERSE)) {
666
+ reverse = true;
667
+ this.advance();
668
+ }
669
+
670
+ const name = this.parseAttributeName();
671
+
672
+ // Parse comparison operator and value
673
+ let comparison = null;
674
+ if (this.match(ECLTokenType.EQUALS, ECLTokenType.NOT_EQUALS)) {
675
+ const operator = this.current;
676
+ this.advance();
677
+ const value = this.parseSubExpressionConstraint();
678
+
679
+ comparison = {
680
+ type: ECLNodeType.EXPRESSION_COMPARISON,
681
+ operator: operator.type,
682
+ value
683
+ };
684
+ } else if (this.match(ECLTokenType.LT, ECLTokenType.LTE, ECLTokenType.CHILD_OF, ECLTokenType.PARENT_OF, ECLTokenType.GTE)) {
685
+ // Note: CHILD_OF (<) is treated as LT and PARENT_OF (>) is treated as GT in numeric comparison context
686
+ const operator = this.current;
687
+ this.advance();
688
+
689
+ this.expect(ECLTokenType.HASH);
690
+
691
+ let value;
692
+ if (this.match(ECLTokenType.SCTID, ECLTokenType.DECIMAL, ECLTokenType.INTEGER)) {
693
+ // In numeric comparison context, accept SCTID, DECIMAL, or INTEGER as numbers
694
+ value = this.current.value;
695
+ this.advance();
696
+ } else {
697
+ this.error('Expected numeric value after #');
698
+ }
699
+
700
+ // Map CHILD_OF to LT and PARENT_OF to GT for numeric comparisons
701
+ let operatorType = operator.type;
702
+ if (operator.type === ECLTokenType.CHILD_OF) {
703
+ operatorType = ECLTokenType.LT;
704
+ } else if (operator.type === ECLTokenType.PARENT_OF) {
705
+ operatorType = ECLTokenType.GT;
706
+ }
707
+
708
+ comparison = {
709
+ type: ECLNodeType.NUMERIC_COMPARISON,
710
+ operator: operatorType,
711
+ value
712
+ };
713
+ }
714
+
715
+ return {
716
+ type: ECLNodeType.ATTRIBUTE,
717
+ cardinality,
718
+ reverse,
719
+ name,
720
+ comparison
721
+ };
722
+ }
723
+
724
+ parseAttributeName() {
725
+ return this.parseEclFocusConcept();
726
+ }
727
+
728
+ parseCardinality() {
729
+ this.expect(ECLTokenType.LBRACKET);
730
+
731
+ let min = null;
732
+ let max = null;
733
+
734
+ if (this.match(ECLTokenType.SCTID)) {
735
+ // Parse as number in cardinality context
736
+ min = parseInt(this.current.value);
737
+ this.advance();
738
+
739
+ // Check for range syntax: ..
740
+ if (this.match(ECLTokenType.CARDINALITY_RANGE)) {
741
+ this.advance(); // consume ..
742
+
743
+ if (this.match(ECLTokenType.SCTID)) {
744
+ max = parseInt(this.current.value);
745
+ this.advance();
746
+ } else if (this.match(ECLTokenType.WILDCARD)) {
747
+ max = '*';
748
+ this.advance();
749
+ } else {
750
+ this.error('Expected number or * after ..');
751
+ }
752
+ } else {
753
+ max = min; // Single number means exact cardinality
754
+ }
755
+ } else if (this.match(ECLTokenType.WILDCARD)) {
756
+ min = 0;
757
+ max = '*';
758
+ this.advance();
759
+ } else {
760
+ this.error('Expected number or * for cardinality');
761
+ }
762
+
763
+ this.expect(ECLTokenType.RBRACKET);
764
+
765
+ return {
766
+ type: ECLNodeType.CARDINALITY,
767
+ min,
768
+ max
769
+ };
770
+ }
771
+ }
772
+
773
+ /**
774
+ * ECL Validator - Validates and evaluates ECL expressions
775
+ */
776
+ class ECLValidator {
777
+ constructor(snomedServices) {
778
+ this.sct = snomedServices;
779
+ }
780
+
781
+ /**
782
+ * Parse and validate an ECL expression
783
+ */
784
+ parse(eclExpression) {
785
+ try {
786
+ const lexer = new ECLLexer(eclExpression);
787
+ const tokens = lexer.tokenize();
788
+
789
+ const parser = new ECLParser(tokens);
790
+ const ast = parser.parse();
791
+
792
+ // Validate the AST
793
+ this.validateAST(ast);
794
+
795
+ return {
796
+ success: true,
797
+ ast,
798
+ errors: []
799
+ };
800
+ } catch (error) {
801
+ return {
802
+ success: false,
803
+ ast: null,
804
+ errors: [error.message]
805
+ };
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Validate AST node semantically
811
+ */
812
+ validateAST(node) {
813
+ if (!node || typeof node !== 'object') {
814
+ throw new Error('Invalid AST node');
815
+ }
816
+
817
+ switch (node.type) {
818
+ case ECLNodeType.CONCEPT_REFERENCE:
819
+ this.validateConceptReference(node);
820
+ break;
821
+
822
+ case ECLNodeType.REFINED_EXPRESSION_CONSTRAINT:
823
+ this.validateAST(node.base);
824
+ this.validateAST(node.refinement);
825
+ break;
826
+
827
+ case ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT:
828
+ this.validateAST(node.left);
829
+ this.validateAST(node.right);
830
+ break;
831
+
832
+ case ECLNodeType.DOTTED_EXPRESSION_CONSTRAINT:
833
+ this.validateAST(node.base);
834
+ node.attributes.forEach(attr => this.validateAST(attr));
835
+ break;
836
+
837
+ case ECLNodeType.SUB_EXPRESSION_CONSTRAINT:
838
+ this.validateAST(node.focus);
839
+ break;
840
+
841
+ case ECLNodeType.MEMBER_OF:
842
+ this.validateAST(node.refSet);
843
+ this.validateReferenceSet(node.refSet);
844
+ break;
845
+
846
+ case ECLNodeType.ATTRIBUTE_SET:
847
+ node.attributes.forEach(attr => this.validateAST(attr));
848
+ break;
849
+
850
+ case ECLNodeType.ATTRIBUTE_GROUP:
851
+ node.attributes.forEach(attr => this.validateAST(attr));
852
+ break;
853
+
854
+ case ECLNodeType.ATTRIBUTE:
855
+ this.validateAST(node.name);
856
+ if (node.comparison) {
857
+ this.validateComparison(node.comparison);
858
+ }
859
+ break;
860
+
861
+ case ECLNodeType.EXPRESSION_COMPARISON:
862
+ this.validateAST(node.value);
863
+ break;
864
+
865
+ case ECLNodeType.WILDCARD:
866
+ // Wildcards are always valid
867
+ break;
868
+
869
+ default:
870
+ // Allow other node types to pass through
871
+ break;
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Validate concept reference exists in SNOMED CT
877
+ */
878
+ /**
879
+ * Validate concept reference exists in SNOMED CT and term matches if provided
880
+ */
881
+ validateConceptReference(node) {
882
+ if (!node.conceptId) {
883
+ throw new Error('Concept reference missing concept ID');
884
+ }
885
+
886
+ if (!/^\d+$/.test(node.conceptId)) {
887
+ throw new Error(`Invalid SNOMED CT concept ID format: ${node.conceptId}`);
888
+ }
889
+
890
+ // Check if it's a valid concept ID
891
+ try {
892
+ if (!this.sct.conceptExists(node.conceptId)) {
893
+ // Check if it might be a number used in wrong context
894
+ const numValue = parseInt(node.conceptId);
895
+ if (numValue < 1000000) {
896
+ throw new Error(`${node.conceptId} appears to be a number rather than a SNOMED CT concept ID. SNOMED concept IDs are typically 6+ digits. If this should be a numeric value, use it in a cardinality [${node.conceptId}] or comparison >= #${node.conceptId} context.`);
897
+ } else {
898
+ throw new Error(`SNOMED CT concept ${node.conceptId} not found in the loaded terminology`);
899
+ }
900
+ }
901
+ } catch (error) {
902
+ if (error.message.includes('not found')) {
903
+ throw error; // Re-throw our custom message
904
+ }
905
+ throw new Error(`Error validating concept ${node.conceptId}: ${error.message}`);
906
+ }
907
+
908
+ // Validate term if provided
909
+ if (node.term) {
910
+ try {
911
+ const conceptIndex = this.getConceptReference(node.conceptId);
912
+ const concept = this.sct.concepts.getConcept(conceptIndex);
913
+ const descriptionsRef = concept.descriptions;
914
+
915
+ if (descriptionsRef === 0) {
916
+ throw new Error(`Concept ${node.conceptId} has no descriptions`);
917
+ }
918
+
919
+ const descriptionIndices = this.sct.refs.getReferences(descriptionsRef);
920
+ let termFound = false;
921
+
922
+ const list = [];
923
+ // Check if the provided term matches any of the concept's descriptions
924
+ for (const descIndex of descriptionIndices) {
925
+ const description = this.sct.descriptions.getDescription(descIndex);
926
+ const actualTerm = this.sct.strings.getEntry(description.iDesc).trim();
927
+ list.push(actualTerm);
928
+ if (actualTerm === node.term.trim()) {
929
+ termFound = true;
930
+ break;
931
+ }
932
+ }
933
+
934
+ if (!termFound) {
935
+ // Get the preferred term for a more helpful error message
936
+ const preferredTerm = this.sct.getDisplayName(conceptIndex);
937
+ throw new Error(`Term "${node.term}" does not match any active description for concept ${node.conceptId}. Expected term like "${preferredTerm}" or from ${list}`);
938
+ }
939
+ } catch (error) {
940
+ if (error.message.includes('does not match')) {
941
+ throw error; // Re-throw our term validation error
942
+ }
943
+ throw new Error(`Error validating term for concept ${node.conceptId}: ${error.message}`);
944
+ }
945
+ }
946
+ }
947
+ /**
948
+ * Validate reference set concept
949
+ */
950
+ validateReferenceSet(node) {
951
+ if (node.type === ECLNodeType.CONCEPT_REFERENCE) {
952
+ const conceptIndex = this.getConceptReference(node.conceptId);
953
+ const refSetIndex = this.sct.getConceptRefSet(conceptIndex, false);
954
+ if (refSetIndex === 0) {
955
+ throw new Error(`Concept ${node.conceptId} is not a reference set`);
956
+ }
957
+ }
958
+ }
959
+
960
+ /**
961
+ * Validate comparison expressions
962
+ */
963
+ validateComparison(comparison) {
964
+ switch (comparison.type) {
965
+ case ECLNodeType.EXPRESSION_COMPARISON:
966
+ this.validateAST(comparison.value);
967
+ break;
968
+
969
+ case ECLNodeType.NUMERIC_COMPARISON:
970
+ if (!/^-?\d+(\.\d+)?$/.test(comparison.value)) {
971
+ throw new Error(`Invalid numeric value: ${comparison.value}`);
972
+ }
973
+ break;
974
+
975
+ case ECLNodeType.STRING_COMPARISON:
976
+ // String values are always valid
977
+ break;
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Evaluate ECL expression and return matching concepts
983
+ */
984
+ async evaluate(eclExpression, options = {}) {
985
+ const parseResult = this.parse(eclExpression);
986
+
987
+ if (!parseResult.success) {
988
+ throw new Error(`ECL parsing failed: ${parseResult.errors.join(', ')}`);
989
+ }
990
+
991
+ const filterContext = await this.evaluateAST(parseResult.ast, options);
992
+ return this.convertFilterToResults(filterContext);
993
+ }
994
+
995
+ /**
996
+ * Evaluate AST node and return filter context
997
+ */
998
+ async evaluateAST(node = {}) {
999
+ if (!node) {
1000
+ throw new Error('Cannot evaluate null AST node');
1001
+ }
1002
+
1003
+ switch (node.type) {
1004
+ case ECLNodeType.CONCEPT_REFERENCE:
1005
+ return await this.evaluateConceptReference(node);
1006
+
1007
+ case ECLNodeType.WILDCARD:
1008
+ return await this.evaluateWildcard();
1009
+
1010
+ case ECLNodeType.SUB_EXPRESSION_CONSTRAINT:
1011
+ return await this.evaluateSubExpressionConstraint(node);
1012
+
1013
+ case ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT:
1014
+ return await this.evaluateCompoundExpression(node);
1015
+
1016
+ case ECLNodeType.REFINED_EXPRESSION_CONSTRAINT:
1017
+ return await this.evaluateRefinedExpression(node);
1018
+
1019
+ case ECLNodeType.MEMBER_OF:
1020
+ return await this.evaluateMemberOf(node);
1021
+
1022
+ default:
1023
+ throw new Error(`Unsupported AST node type: ${node.type}`);
1024
+ }
1025
+ }
1026
+
1027
+ async evaluateConceptReference(node) {
1028
+ const conceptId = this.sct.stringToId(node.conceptId);
1029
+ return this.sct.filterEquals(conceptId);
1030
+ }
1031
+
1032
+ async evaluateWildcard() {
1033
+ // Return all concepts - this would need optimization in practice
1034
+ const filter = new SnomedFilterContext();
1035
+ const allConcepts = [];
1036
+
1037
+ for (let i = 0; i < this.sct.concepts.count(); i++) {
1038
+ const conceptIndex = i * this.sct.concepts.constructor.CONCEPT_SIZE;
1039
+ if (this.sct.isActive(conceptIndex)) {
1040
+ allConcepts.push(conceptIndex);
1041
+ }
1042
+ }
1043
+
1044
+ filter.descendants = allConcepts;
1045
+ return filter;
1046
+ }
1047
+
1048
+ async evaluateSubExpressionConstraint(node) {
1049
+ const baseFilter = await this.evaluateAST(node.focus);
1050
+
1051
+ if (!node.operator) {
1052
+ return baseFilter;
1053
+ }
1054
+
1055
+ // Apply constraint operator
1056
+ const results = new SnomedFilterContext();
1057
+
1058
+ for (const conceptIndex of baseFilter.descendants || []) {
1059
+ const conceptId = this.sct.concepts.getConceptId(conceptIndex);
1060
+
1061
+ let operatorFilter;
1062
+ switch (node.operator) {
1063
+ case ECLTokenType.CHILD_OF:
1064
+ operatorFilter = this.sct.filterIsA(conceptId, false);
1065
+ break;
1066
+ case ECLTokenType.CHILD_OR_SELF_OF:
1067
+ operatorFilter = this.sct.filterIsA(conceptId, true);
1068
+ break;
1069
+ case ECLTokenType.DESCENDANT_OF:
1070
+ operatorFilter = this.sct.filterIsA(conceptId, false);
1071
+ break;
1072
+ case ECLTokenType.DESCENDANT_OR_SELF_OF:
1073
+ operatorFilter = this.sct.filterIsA(conceptId, true);
1074
+ break;
1075
+ case ECLTokenType.PARENT_OF:
1076
+ case ECLTokenType.PARENT_OR_SELF_OF:
1077
+ case ECLTokenType.ANCESTOR_OF:
1078
+ case ECLTokenType.ANCESTOR_OR_SELF_OF:
1079
+ // These would require reverse hierarchy traversal
1080
+ throw new Error(`Operator ${node.operator} not yet implemented`);
1081
+ default:
1082
+ throw new Error(`Unknown constraint operator: ${node.operator}`);
1083
+ }
1084
+
1085
+ results.descendants = [...(results.descendants || []), ...(operatorFilter.descendants || [])];
1086
+ }
1087
+
1088
+ return results;
1089
+ }
1090
+
1091
+ async evaluateCompoundExpression(node) {
1092
+ const leftFilter = await this.evaluateAST(node.left);
1093
+ const rightFilter = await this.evaluateAST(node.right);
1094
+
1095
+ const result = new SnomedFilterContext();
1096
+ const leftSet = new Set(leftFilter.descendants || []);
1097
+ const rightSet = new Set(rightFilter.descendants || []);
1098
+
1099
+ switch (node.operator) {
1100
+ case ECLNodeType.CONJUNCTION:
1101
+ result.descendants = [...leftSet].filter(x => rightSet.has(x));
1102
+ break;
1103
+ case ECLNodeType.DISJUNCTION:
1104
+ result.descendants = [...new Set([...leftSet, ...rightSet])];
1105
+ break;
1106
+ case ECLNodeType.EXCLUSION:
1107
+ result.descendants = [...leftSet].filter(x => !rightSet.has(x));
1108
+ break;
1109
+ default:
1110
+ throw new Error(`Unknown compound operator: ${node.operator}`);
1111
+ }
1112
+
1113
+ return result;
1114
+ }
1115
+
1116
+ async evaluateRefinedExpression(node) {
1117
+ // This is a simplified implementation
1118
+ // Full refinement evaluation would require analyzing concept relationships
1119
+ const baseFilter = await this.evaluateAST(node.base);
1120
+
1121
+ // For now, return the base filter
1122
+ // TODO: Implement refinement filtering based on node.refinement
1123
+ return baseFilter;
1124
+ }
1125
+
1126
+ async evaluateMemberOf(node) {
1127
+ const refSetFilter = await this.evaluateAST(node.refSet);
1128
+
1129
+ if (refSetFilter.descendants && refSetFilter.descendants.length > 0) {
1130
+ const conceptIndex = refSetFilter.descendants[0];
1131
+ return this.sct.filterIn(this.sct.concepts.getConceptId(conceptIndex));
1132
+ }
1133
+
1134
+ throw new Error('Invalid reference set for memberOf operation');
1135
+ }
1136
+
1137
+ /**
1138
+ * Convert filter context to user-friendly results
1139
+ */
1140
+ convertFilterToResults(filterContext) {
1141
+ const results = [];
1142
+
1143
+ const concepts = filterContext.descendants || filterContext.matches || [];
1144
+
1145
+ for (const conceptIndex of concepts.slice(0, 1000)) { // Limit results
1146
+ try {
1147
+ let conceptId, term;
1148
+
1149
+ if (typeof conceptIndex === 'object' && conceptIndex.index !== undefined) {
1150
+ // From search results
1151
+ conceptId = conceptIndex.term;
1152
+ term = this.sct.getDisplayName(conceptIndex.index);
1153
+ } else {
1154
+ // From regular index
1155
+ conceptId = this.sct.concepts.getConceptId(conceptIndex);
1156
+ term = this.sct.getDisplayName(conceptIndex);
1157
+ }
1158
+
1159
+ results.push({
1160
+ conceptId: conceptId.toString(),
1161
+ term,
1162
+ active: this.sct.isActive(conceptIndex)
1163
+ });
1164
+ } catch (error) {
1165
+ // Skip concepts that can't be read
1166
+ continue;
1167
+ }
1168
+ }
1169
+
1170
+ return {
1171
+ total: concepts.length,
1172
+ results
1173
+ };
1174
+ }
1175
+
1176
+ /**
1177
+ * Helper to get concept reference index from concept ID
1178
+ */
1179
+ getConceptReference(conceptId) {
1180
+ const id = this.sct.stringToId(conceptId);
1181
+ const result = this.sct.concepts.findConcept(id);
1182
+ if (!result.found) {
1183
+ throw new Error(`Concept ${conceptId} not found`);
1184
+ }
1185
+ return result.index;
1186
+ }
1187
+
1188
+ /**
1189
+ * Validate ECL syntax only (no semantic validation)
1190
+ */
1191
+ validateSyntax(eclExpression) {
1192
+ try {
1193
+ const lexer = new ECLLexer(eclExpression);
1194
+ const tokens = lexer.tokenize();
1195
+
1196
+ const parser = new ECLParser(tokens);
1197
+ const ast = parser.parse();
1198
+
1199
+ return {
1200
+ success: true,
1201
+ ast,
1202
+ errors: []
1203
+ };
1204
+ } catch (error) {
1205
+ return {
1206
+ success: false,
1207
+ ast: null,
1208
+ errors: [error.message]
1209
+ };
1210
+ }
1211
+ }
1212
+
1213
+ /**
1214
+ * Get examples of valid ECL expressions
1215
+ */
1216
+ getExamples() {
1217
+ return [
1218
+ // Simple concept
1219
+ '73211009 |Diabetes mellitus|',
1220
+
1221
+ // Descendants
1222
+ '< 73211009 |Diabetes mellitus|',
1223
+ '<< 404684003 |Clinical finding|',
1224
+
1225
+ // Wildcards
1226
+ '*',
1227
+
1228
+ // Boolean operations
1229
+ '<< 19829001 |Disorder of lung| AND << 301867009 |Edema of trunk|',
1230
+ '<< 126537000 |Neoplasm of bone| OR << 92691004 |Secondary malignant neoplasm of bone|',
1231
+
1232
+ // Refinements
1233
+ '<< 404684003 |Clinical finding|: 116676008 |Associated morphology| = 72704001 |Fracture|',
1234
+
1235
+ // Attribute groups
1236
+ '<< 404684003 |Clinical finding|: {116676008 |Associated morphology| = 72704001 |Fracture|, 363698007 |Finding site| = << 71341001 |Bone structure|}',
1237
+
1238
+ // Member of reference set
1239
+ '^ 447562003 |ICD-10 complex map reference set|',
1240
+
1241
+ // Dotted attributes
1242
+ '<< 404684003 |Clinical finding|.116676008 |Associated morphology|'
1243
+ ];
1244
+ }
1245
+
1246
+ static CONCEPT_MODEL_ATTRIBUTE = '410662002'; // |Concept model attribute|
1247
+ static CLINICAL_FINDING = '404684003'; // |Clinical finding|
1248
+ static PROCEDURE = '71388002'; // |Procedure|
1249
+
1250
+ /**
1251
+ * Perform semantic validation on a parsed AST
1252
+ * This is separate from parse() and optional
1253
+ */
1254
+ validateSemantics(ast) {
1255
+ const errors = [];
1256
+ this.validateSemanticAST(ast, errors);
1257
+
1258
+ return {
1259
+ success: errors.length === 0,
1260
+ errors
1261
+ };
1262
+ }
1263
+
1264
+ /**
1265
+ * Parse AND validate semantics in one call
1266
+ */
1267
+ parseAndValidateSemantics(eclExpression) {
1268
+ const parseResult = this.parse(eclExpression);
1269
+
1270
+ if (!parseResult.success) {
1271
+ return parseResult;
1272
+ }
1273
+
1274
+ const semanticResult = this.validateSemantics(parseResult.ast);
1275
+
1276
+ return {
1277
+ success: parseResult.success && semanticResult.success,
1278
+ ast: parseResult.ast,
1279
+ errors: [...parseResult.errors, ...semanticResult.errors]
1280
+ };
1281
+ }
1282
+
1283
+ /**
1284
+ * Semantic validation traversal (separate from basic validateAST)
1285
+ */
1286
+ validateSemanticAST(node, errors) {
1287
+ if (!node || typeof node !== 'object') {
1288
+ return;
1289
+ }
1290
+
1291
+ switch (node.type) {
1292
+ case ECLNodeType.REFINED_EXPRESSION_CONSTRAINT:
1293
+ this.validateRefinedExpressionSemantics(node, errors);
1294
+ break;
1295
+
1296
+ case ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT:
1297
+ this.validateSemanticAST(node.left, errors);
1298
+ this.validateSemanticAST(node.right, errors);
1299
+ break;
1300
+
1301
+ case ECLNodeType.DOTTED_EXPRESSION_CONSTRAINT:
1302
+ this.validateSemanticAST(node.base, errors);
1303
+ node.attributes.forEach(attr => this.validateSemanticAST(attr, errors));
1304
+ break;
1305
+
1306
+ case ECLNodeType.SUB_EXPRESSION_CONSTRAINT:
1307
+ this.validateSemanticAST(node.focus, errors);
1308
+ break;
1309
+
1310
+ case ECLNodeType.MEMBER_OF:
1311
+ this.validateSemanticAST(node.refSet, errors);
1312
+ break;
1313
+
1314
+ case ECLNodeType.ATTRIBUTE_SET:
1315
+ node.attributes.forEach(attr => this.validateSemanticAST(attr, errors));
1316
+ break;
1317
+
1318
+ case ECLNodeType.ATTRIBUTE_GROUP:
1319
+ node.attributes.forEach(attr => this.validateSemanticAST(attr, errors));
1320
+ break;
1321
+
1322
+ case ECLNodeType.ATTRIBUTE:
1323
+ this.validateAttributeSemantics(node, errors);
1324
+ break;
1325
+
1326
+ case ECLNodeType.EXPRESSION_COMPARISON:
1327
+ this.validateSemanticAST(node.value, errors);
1328
+ break;
1329
+
1330
+ // Basic nodes don't need semantic validation
1331
+ case ECLNodeType.CONCEPT_REFERENCE:
1332
+ case ECLNodeType.WILDCARD:
1333
+ break;
1334
+
1335
+ default:
1336
+ // Allow unknown node types to pass through
1337
+ break;
1338
+ }
1339
+ }
1340
+
1341
+ /**
1342
+ * Validate semantics of refined expressions
1343
+ */
1344
+ validateRefinedExpressionSemantics(node, errors) {
1345
+ this.validateSemanticAST(node.base, errors);
1346
+ this.validateSemanticAST(node.refinement, errors);
1347
+
1348
+ // Get the base concept(s) being refined
1349
+ const baseConcepts = this.extractBaseConceptIds(node.base);
1350
+ const attributes = this.extractAttributesFromRefinement(node.refinement);
1351
+
1352
+ // Validate each attribute in context
1353
+ for (const attr of attributes) {
1354
+ this.validateAttributeInContext(attr, baseConcepts, errors);
1355
+ }
1356
+ }
1357
+
1358
+ /**
1359
+ * Validate attribute semantics
1360
+ */
1361
+ validateAttributeSemantics(node, errors) {
1362
+ // Validate the attribute name is a relationship type
1363
+ this.validateRelationshipType(node.name, errors);
1364
+
1365
+ // Validate the comparison if present
1366
+ if (node.comparison && node.comparison.value) {
1367
+ this.validateSemanticAST(node.comparison.value, errors);
1368
+ this.validateAttributeRange(node.name, node.comparison.value, errors);
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * Check if concept is a valid relationship type
1374
+ */
1375
+ validateRelationshipType(attributeNode, errors) {
1376
+ if (attributeNode.type !== ECLNodeType.CONCEPT_REFERENCE) {
1377
+ return; // Skip validation for wildcards or complex expressions
1378
+ }
1379
+
1380
+ try {
1381
+ const conceptId = attributeNode.conceptId;
1382
+ const conceptIndex = this.getConceptReference(conceptId);
1383
+
1384
+ // Check if it's a descendant of "Concept model attribute"
1385
+ const attributeRootIndex = this.getConceptReference(ECLValidator.CONCEPT_MODEL_ATTRIBUTE);
1386
+
1387
+ if (!this.sct.subsumes(attributeRootIndex, conceptIndex)) {
1388
+ const displayName = attributeNode.term || this.sct.getDisplayName(conceptIndex);
1389
+ errors.push(`Concept ${conceptId} |${displayName}| is not a valid relationship type. Relationship types must be descendants of ${ECLValidator.CONCEPT_MODEL_ATTRIBUTE} |Concept model attribute|`);
1390
+ }
1391
+ } catch (error) {
1392
+ errors.push(`Error validating relationship type ${attributeNode.conceptId}: ${error.message}`);
1393
+ }
1394
+ }
1395
+
1396
+ /**
1397
+ * Validate attribute usage in context of base concepts
1398
+ */
1399
+ validateAttributeInContext(attribute, baseConcepts, errors) {
1400
+ // Check domain appropriateness
1401
+ for (const baseConcept of baseConcepts) {
1402
+ this.validateAttributeDomain(attribute.name, baseConcept, errors);
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Validate attribute domain (which concepts can use this attribute)
1408
+ */
1409
+ validateAttributeDomain(attributeNode, baseConceptIndex, errors) {
1410
+ if (attributeNode.type !== ECLNodeType.CONCEPT_REFERENCE) {
1411
+ return;
1412
+ }
1413
+
1414
+ try {
1415
+ const attributeId = attributeNode.conceptId;
1416
+
1417
+ // Define common domain restrictions
1418
+ const domainRules = {
1419
+ '116676008': ['404684003'], // |Associated morphology| -> |Clinical finding|
1420
+ '363698007': ['404684003'], // |Finding site| -> |Clinical finding|
1421
+ '42752001': ['71388002'], // |Due to| -> |Procedure|
1422
+ '260686004': ['71388002'], // |Method| -> |Procedure|
1423
+ '405815000': ['71388002'] // |Procedure device| -> |Procedure|
1424
+ };
1425
+
1426
+ if (domainRules[attributeId]) {
1427
+ const allowedHierarchies = domainRules[attributeId];
1428
+ const isValidDomain = allowedHierarchies.some(hierarchyId => {
1429
+ const hierarchyIndex = this.getConceptReference(hierarchyId);
1430
+ return this.sct.subsumes(hierarchyIndex, baseConceptIndex);
1431
+ });
1432
+
1433
+ if (!isValidDomain) {
1434
+ const hierarchyNames = allowedHierarchies.map(id => {
1435
+ const idx = this.getConceptReference(id);
1436
+ return this.sct.getDisplayName(idx);
1437
+ }).join(', ');
1438
+
1439
+ const displayName = attributeNode.term || this.sct.getDisplayName(this.getConceptReference(attributeId));
1440
+ errors.push(`Attribute ${attributeId} |${displayName}| is not typically used with concepts outside of: ${hierarchyNames}`);
1441
+ }
1442
+ }
1443
+ } catch (error) {
1444
+ errors.push(`Error validating attribute domain for ${attributeNode.conceptId}: ${error.message}`);
1445
+ }
1446
+ }
1447
+
1448
+ /**
1449
+ * Validate attribute range (what values are allowed)
1450
+ */
1451
+ validateAttributeRange(attributeNode, valueNode, errors) {
1452
+ if (attributeNode.type !== ECLNodeType.CONCEPT_REFERENCE) {
1453
+ return;
1454
+ }
1455
+
1456
+ try {
1457
+ // Extract the actual concept reference from the value node
1458
+ let actualValueNode = valueNode;
1459
+ if (valueNode.type === ECLNodeType.SUB_EXPRESSION_CONSTRAINT) {
1460
+ actualValueNode = valueNode.focus;
1461
+ }
1462
+
1463
+ if (actualValueNode.type !== ECLNodeType.CONCEPT_REFERENCE) {
1464
+ return; // Skip validation for wildcards or complex expressions
1465
+ }
1466
+
1467
+ const attributeId = attributeNode.conceptId;
1468
+ const valueId = actualValueNode.conceptId;
1469
+
1470
+ // Define common range restrictions
1471
+ const rangeRules = {
1472
+ '116676008': ['49755003'], // |Associated morphology| -> |Morphologically abnormal structure|
1473
+ '363698007': ['442083009'], // |Finding site| -> |Anatomical or acquired body structure|
1474
+ '42752001': ['404684003'], // |Due to| -> |Clinical finding|
1475
+ '47429007': ['78621006'] // |Associated with| -> |Physical force|
1476
+ };
1477
+
1478
+ if (rangeRules[attributeId]) {
1479
+ const allowedRanges = rangeRules[attributeId];
1480
+ const valueIndex = this.getConceptReference(valueId);
1481
+
1482
+ const isValidRange = allowedRanges.some(rangeId => {
1483
+ const rangeIndex = this.getConceptReference(rangeId);
1484
+ return this.sct.subsumes(rangeIndex, valueIndex);
1485
+ });
1486
+
1487
+ if (!isValidRange) {
1488
+ const rangeNames = allowedRanges.map(id => {
1489
+ const idx = this.getConceptReference(id);
1490
+ return this.sct.getDisplayName(idx);
1491
+ }).join(', ');
1492
+
1493
+ const valueDisplayName = actualValueNode.term || this.sct.getDisplayName(valueIndex);
1494
+ errors.push(`Value ${valueId} |${valueDisplayName}| is not a valid concept for attribute ${attributeId}. Expected values from: ${rangeNames}`);
1495
+ }
1496
+ }
1497
+ } catch (error) {
1498
+ errors.push(`Error validating attribute range for ${attributeNode.conceptId}: ${error.message}`);
1499
+ }
1500
+ }
1501
+
1502
+ /**
1503
+ * Extract base concept IDs from expression constraint
1504
+ */
1505
+ extractBaseConceptIds(node) {
1506
+ const concepts = [];
1507
+
1508
+ switch (node.type) {
1509
+ case ECLNodeType.SUB_EXPRESSION_CONSTRAINT:
1510
+ concepts.push(...this.extractBaseConceptIds(node.focus));
1511
+ break;
1512
+ case ECLNodeType.CONCEPT_REFERENCE:
1513
+ concepts.push(this.getConceptReference(node.conceptId));
1514
+ break;
1515
+ case ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT:
1516
+ concepts.push(...this.extractBaseConceptIds(node.left));
1517
+ concepts.push(...this.extractBaseConceptIds(node.right));
1518
+ break;
1519
+ case ECLNodeType.MEMBER_OF:
1520
+ concepts.push(...this.extractBaseConceptIds(node.refSet));
1521
+ break;
1522
+ case ECLNodeType.WILDCARD:
1523
+ // Wildcards represent all concepts - skip semantic validation
1524
+ break;
1525
+ }
1526
+
1527
+ return concepts;
1528
+ }
1529
+
1530
+ /**
1531
+ * Extract attributes from refinement
1532
+ */
1533
+ extractAttributesFromRefinement(refinementNode) {
1534
+ const attributes = [];
1535
+
1536
+ switch (refinementNode.type) {
1537
+ case ECLNodeType.ATTRIBUTE:
1538
+ attributes.push(refinementNode);
1539
+ break;
1540
+ case ECLNodeType.ATTRIBUTE_SET:
1541
+ attributes.push(...refinementNode.attributes);
1542
+ break;
1543
+ case ECLNodeType.ATTRIBUTE_GROUP:
1544
+ attributes.push(...refinementNode.attributes);
1545
+ break;
1546
+ }
1547
+
1548
+ return attributes;
1549
+ }
1550
+
1551
+
1552
+ }
1553
+
1554
+ module.exports = {
1555
+ ECLValidator,
1556
+ ECLLexer,
1557
+ ECLParser,
1558
+ ECLTokenType,
1559
+ ECLNodeType
1560
+ };