@the-cascade-protocol/cli 0.2.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 (165) hide show
  1. package/.dockerignore +7 -0
  2. package/.eslintrc.json +23 -0
  3. package/.prettierrc +7 -0
  4. package/DOCKER.md +36 -0
  5. package/Dockerfile +18 -0
  6. package/README.md +69 -0
  7. package/dist/commands/capabilities.d.ts +9 -0
  8. package/dist/commands/capabilities.d.ts.map +1 -0
  9. package/dist/commands/capabilities.js +194 -0
  10. package/dist/commands/capabilities.js.map +1 -0
  11. package/dist/commands/conformance.d.ts +15 -0
  12. package/dist/commands/conformance.d.ts.map +1 -0
  13. package/dist/commands/conformance.js +348 -0
  14. package/dist/commands/conformance.js.map +1 -0
  15. package/dist/commands/convert.d.ts +21 -0
  16. package/dist/commands/convert.d.ts.map +1 -0
  17. package/dist/commands/convert.js +134 -0
  18. package/dist/commands/convert.js.map +1 -0
  19. package/dist/commands/pod/export.d.ts +8 -0
  20. package/dist/commands/pod/export.d.ts.map +1 -0
  21. package/dist/commands/pod/export.js +72 -0
  22. package/dist/commands/pod/export.js.map +1 -0
  23. package/dist/commands/pod/helpers.d.ts +79 -0
  24. package/dist/commands/pod/helpers.d.ts.map +1 -0
  25. package/dist/commands/pod/helpers.js +369 -0
  26. package/dist/commands/pod/helpers.js.map +1 -0
  27. package/dist/commands/pod/index.d.ts +20 -0
  28. package/dist/commands/pod/index.d.ts.map +1 -0
  29. package/dist/commands/pod/index.js +29 -0
  30. package/dist/commands/pod/index.js.map +1 -0
  31. package/dist/commands/pod/info.d.ts +9 -0
  32. package/dist/commands/pod/info.d.ts.map +1 -0
  33. package/dist/commands/pod/info.js +196 -0
  34. package/dist/commands/pod/info.js.map +1 -0
  35. package/dist/commands/pod/init.d.ts +9 -0
  36. package/dist/commands/pod/init.d.ts.map +1 -0
  37. package/dist/commands/pod/init.js +251 -0
  38. package/dist/commands/pod/init.js.map +1 -0
  39. package/dist/commands/pod/query.d.ts +9 -0
  40. package/dist/commands/pod/query.d.ts.map +1 -0
  41. package/dist/commands/pod/query.js +169 -0
  42. package/dist/commands/pod/query.js.map +1 -0
  43. package/dist/commands/pod 2.js +1017 -0
  44. package/dist/commands/pod.d.ts +28 -0
  45. package/dist/commands/pod.d.ts 2.map +1 -0
  46. package/dist/commands/pod.d.ts.map +1 -0
  47. package/dist/commands/pod.js +1031 -0
  48. package/dist/commands/pod.js 2.map +1 -0
  49. package/dist/commands/pod.js.map +1 -0
  50. package/dist/commands/serve.d.ts +33 -0
  51. package/dist/commands/serve.d.ts.map +1 -0
  52. package/dist/commands/serve.js +74 -0
  53. package/dist/commands/serve.js.map +1 -0
  54. package/dist/commands/validate.d.ts +18 -0
  55. package/dist/commands/validate.d.ts.map +1 -0
  56. package/dist/commands/validate.js +275 -0
  57. package/dist/commands/validate.js.map +1 -0
  58. package/dist/index.d.ts +19 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +49 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
  63. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
  64. package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
  65. package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
  66. package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
  67. package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
  68. package/dist/lib/fhir-converter/converters-clinical.js +391 -0
  69. package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
  70. package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
  71. package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
  72. package/dist/lib/fhir-converter/converters-demographics.js +242 -0
  73. package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
  74. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
  75. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
  76. package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
  77. package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
  78. package/dist/lib/fhir-converter/index.d.ts +36 -0
  79. package/dist/lib/fhir-converter/index.d.ts.map +1 -0
  80. package/dist/lib/fhir-converter/index.js +187 -0
  81. package/dist/lib/fhir-converter/index.js.map +1 -0
  82. package/dist/lib/fhir-converter/types.d.ts +77 -0
  83. package/dist/lib/fhir-converter/types.d.ts.map +1 -0
  84. package/dist/lib/fhir-converter/types.js +236 -0
  85. package/dist/lib/fhir-converter/types.js.map +1 -0
  86. package/dist/lib/fhir-converter.d.ts +62 -0
  87. package/dist/lib/fhir-converter.d.ts.map +1 -0
  88. package/dist/lib/fhir-converter.js +1474 -0
  89. package/dist/lib/fhir-converter.js.map +1 -0
  90. package/dist/lib/mcp/audit.d.ts +24 -0
  91. package/dist/lib/mcp/audit.d.ts.map +1 -0
  92. package/dist/lib/mcp/audit.js +85 -0
  93. package/dist/lib/mcp/audit.js.map +1 -0
  94. package/dist/lib/mcp/server.d.ts +38 -0
  95. package/dist/lib/mcp/server.d.ts.map +1 -0
  96. package/dist/lib/mcp/server.js +172 -0
  97. package/dist/lib/mcp/server.js.map +1 -0
  98. package/dist/lib/mcp/tools.d.ts +47 -0
  99. package/dist/lib/mcp/tools.d.ts.map +1 -0
  100. package/dist/lib/mcp/tools.js +547 -0
  101. package/dist/lib/mcp/tools.js.map +1 -0
  102. package/dist/lib/output.d.ts +26 -0
  103. package/dist/lib/output.d.ts.map +1 -0
  104. package/dist/lib/output.js +64 -0
  105. package/dist/lib/output.js.map +1 -0
  106. package/dist/lib/shacl-validator.d.ts +53 -0
  107. package/dist/lib/shacl-validator.d.ts.map +1 -0
  108. package/dist/lib/shacl-validator.js +245 -0
  109. package/dist/lib/shacl-validator.js.map +1 -0
  110. package/dist/lib/turtle-parser.d.ts +64 -0
  111. package/dist/lib/turtle-parser.d.ts.map +1 -0
  112. package/dist/lib/turtle-parser.js +236 -0
  113. package/dist/lib/turtle-parser.js.map +1 -0
  114. package/dist/shapes/checkup.shapes.ttl +1459 -0
  115. package/dist/shapes/clinical.shapes.ttl +1350 -0
  116. package/dist/shapes/clinical.ttl +1369 -0
  117. package/dist/shapes/core.shapes.ttl +450 -0
  118. package/dist/shapes/core.ttl +603 -0
  119. package/dist/shapes/coverage.shapes.ttl +214 -0
  120. package/dist/shapes/coverage.ttl +182 -0
  121. package/dist/shapes/health.shapes.ttl +697 -0
  122. package/dist/shapes/health.ttl +859 -0
  123. package/dist/shapes/pots.shapes.ttl +481 -0
  124. package/package.json +54 -0
  125. package/src/commands/capabilities.ts +235 -0
  126. package/src/commands/conformance.ts +447 -0
  127. package/src/commands/convert.ts +164 -0
  128. package/src/commands/pod/export.ts +85 -0
  129. package/src/commands/pod/helpers.ts +449 -0
  130. package/src/commands/pod/index.ts +32 -0
  131. package/src/commands/pod/info.ts +239 -0
  132. package/src/commands/pod/init.ts +273 -0
  133. package/src/commands/pod/query.ts +224 -0
  134. package/src/commands/serve.ts +92 -0
  135. package/src/commands/validate.ts +303 -0
  136. package/src/index.ts +58 -0
  137. package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
  138. package/src/lib/fhir-converter/converters-clinical.ts +446 -0
  139. package/src/lib/fhir-converter/converters-demographics.ts +270 -0
  140. package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
  141. package/src/lib/fhir-converter/index.ts +215 -0
  142. package/src/lib/fhir-converter/types.ts +318 -0
  143. package/src/lib/mcp/audit.ts +107 -0
  144. package/src/lib/mcp/server.ts +192 -0
  145. package/src/lib/mcp/tools.ts +668 -0
  146. package/src/lib/output.ts +76 -0
  147. package/src/lib/shacl-validator.ts +314 -0
  148. package/src/lib/turtle-parser.ts +277 -0
  149. package/src/shapes/checkup.shapes.ttl +1459 -0
  150. package/src/shapes/clinical.shapes.ttl +1350 -0
  151. package/src/shapes/clinical.ttl +1369 -0
  152. package/src/shapes/core.shapes.ttl +450 -0
  153. package/src/shapes/core.ttl +603 -0
  154. package/src/shapes/coverage.shapes.ttl +214 -0
  155. package/src/shapes/coverage.ttl +182 -0
  156. package/src/shapes/health.shapes.ttl +697 -0
  157. package/src/shapes/health.ttl +859 -0
  158. package/src/shapes/pots.shapes.ttl +481 -0
  159. package/test-fixtures/fhir-bundle-example.json +216 -0
  160. package/test-fixtures/fhir-medication-example.json +18 -0
  161. package/tests/cli.test.ts +126 -0
  162. package/tests/fhir-converter.test.ts +874 -0
  163. package/tests/mcp-server.test.ts +396 -0
  164. package/tests/pod.test.ts +400 -0
  165. package/tsconfig.json +24 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * FHIR -> Cascade converters for demographics and administrative types.
3
+ *
4
+ * Converts:
5
+ * - Patient -> cascade:PatientProfile
6
+ * - Immunization -> health:ImmunizationRecord
7
+ * - Coverage -> coverage:InsurancePlan
8
+ */
9
+
10
+ import { randomUUID } from 'node:crypto';
11
+ import type { Quad } from 'n3';
12
+
13
+ import {
14
+ type ConversionResult,
15
+ NS,
16
+ extractCodings,
17
+ codeableConceptText,
18
+ tripleStr,
19
+ tripleInt,
20
+ tripleType,
21
+ tripleDateTime,
22
+ tripleDate,
23
+ commonTriples,
24
+ quadsToJsonLd,
25
+ } from './types.js';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Patient converter
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export function convertPatient(resource: any): ConversionResult & { _quads: Quad[] } {
32
+ const warnings: string[] = [];
33
+ const subjectUri = `urn:uuid:${randomUUID()}`;
34
+ const quads: Quad[] = [];
35
+
36
+ quads.push(tripleType(subjectUri, NS.cascade + 'PatientProfile'));
37
+ quads.push(...commonTriples(subjectUri));
38
+
39
+ if (resource.birthDate) {
40
+ quads.push(tripleDate(subjectUri, NS.cascade + 'dateOfBirth', resource.birthDate));
41
+ const dob = new Date(resource.birthDate);
42
+ const now = new Date();
43
+ let age = now.getFullYear() - dob.getFullYear();
44
+ const monthDiff = now.getMonth() - dob.getMonth();
45
+ if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < dob.getDate())) {
46
+ age--;
47
+ }
48
+ quads.push(tripleInt(subjectUri, NS.cascade + 'computedAge', age));
49
+ let ageGroup: string;
50
+ if (age < 18) ageGroup = 'pediatric';
51
+ else if (age < 40) ageGroup = 'young_adult';
52
+ else if (age < 65) ageGroup = 'adult';
53
+ else ageGroup = 'senior';
54
+ quads.push(tripleStr(subjectUri, NS.cascade + 'ageGroup', ageGroup));
55
+ } else {
56
+ warnings.push('No birthDate found in Patient resource');
57
+ }
58
+
59
+ if (resource.gender) {
60
+ const genderMap: Record<string, string> = {
61
+ male: 'male', female: 'female', other: 'intersex', unknown: 'intersex',
62
+ };
63
+ quads.push(tripleStr(subjectUri, NS.cascade + 'biologicalSex', genderMap[resource.gender] ?? resource.gender));
64
+ }
65
+
66
+ if (Array.isArray(resource.address) && resource.address.length > 0) {
67
+ const addr = resource.address[0];
68
+ if (addr.city) quads.push(tripleStr(subjectUri, NS.cascade + 'addressCity', addr.city));
69
+ if (addr.state) quads.push(tripleStr(subjectUri, NS.cascade + 'addressState', addr.state));
70
+ if (addr.postalCode) quads.push(tripleStr(subjectUri, NS.cascade + 'addressPostalCode', addr.postalCode));
71
+ if (addr.country) quads.push(tripleStr(subjectUri, NS.cascade + 'addressCountry', addr.country));
72
+ if (Array.isArray(addr.line)) {
73
+ for (const line of addr.line) {
74
+ quads.push(tripleStr(subjectUri, NS.cascade + 'addressLine', line));
75
+ }
76
+ }
77
+ warnings.push('Patient address flattened onto profile (blank node structure simplified)');
78
+ }
79
+
80
+ if (resource.maritalStatus) {
81
+ const maritalText = codeableConceptText(resource.maritalStatus);
82
+ if (maritalText) {
83
+ const maritalMap: Record<string, string> = {
84
+ S: 'single', M: 'married', D: 'divorced', W: 'widowed',
85
+ A: 'separated', T: 'domestic_partnership', UNK: 'prefer_not_to_say',
86
+ 'Never Married': 'single', 'Married': 'married', 'Divorced': 'divorced',
87
+ 'Widowed': 'widowed', 'Separated': 'separated',
88
+ };
89
+ const code = resource.maritalStatus.coding?.[0]?.code;
90
+ const mapped = maritalMap[code] ?? maritalMap[maritalText] ?? maritalText.toLowerCase();
91
+ quads.push(tripleStr(subjectUri, NS.cascade + 'maritalStatus', mapped));
92
+ }
93
+ }
94
+
95
+ if (resource.id) {
96
+ quads.push(tripleStr(subjectUri, NS.cascade + 'profileId', resource.id));
97
+ }
98
+
99
+ return {
100
+ turtle: '',
101
+ jsonld: quadsToJsonLd(quads, 'cascade:PatientProfile'),
102
+ warnings,
103
+ resourceType: 'Patient',
104
+ cascadeType: 'cascade:PatientProfile',
105
+ _quads: quads,
106
+ };
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Immunization converter
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export function convertImmunization(resource: any): ConversionResult & { _quads: Quad[] } {
114
+ const warnings: string[] = [];
115
+ const subjectUri = `urn:uuid:${randomUUID()}`;
116
+ const quads: Quad[] = [];
117
+
118
+ quads.push(tripleType(subjectUri, NS.health + 'ImmunizationRecord'));
119
+ quads.push(...commonTriples(subjectUri));
120
+
121
+ const vaccineName = codeableConceptText(resource.vaccineCode) ?? 'Unknown Vaccine';
122
+ quads.push(tripleStr(subjectUri, NS.health + 'vaccineName', vaccineName));
123
+
124
+ if (resource.occurrenceDateTime) {
125
+ quads.push(tripleDateTime(subjectUri, NS.health + 'administrationDate', resource.occurrenceDateTime));
126
+ } else if (resource.occurrenceString) {
127
+ warnings.push(`Immunization date is a string: ${resource.occurrenceString}`);
128
+ }
129
+
130
+ quads.push(tripleStr(subjectUri, NS.health + 'status', resource.status ?? 'completed'));
131
+
132
+ const codings = extractCodings(resource.vaccineCode);
133
+ for (const c of codings) {
134
+ if (c.system === 'http://hl7.org/fhir/sid/cvx' || c.system === 'urn:oid:2.16.840.1.113883.12.292') {
135
+ quads.push(tripleStr(subjectUri, NS.health + 'vaccineCode', `CVX-${c.code}`));
136
+ break;
137
+ }
138
+ }
139
+
140
+ if (resource.manufacturer?.display) {
141
+ quads.push(tripleStr(subjectUri, NS.health + 'manufacturer', resource.manufacturer.display));
142
+ }
143
+
144
+ if (resource.lotNumber) {
145
+ quads.push(tripleStr(subjectUri, NS.health + 'lotNumber', resource.lotNumber));
146
+ }
147
+
148
+ if (resource.doseQuantity) {
149
+ const qty = `${resource.doseQuantity.value} ${resource.doseQuantity.unit ?? ''}`.trim();
150
+ quads.push(tripleStr(subjectUri, NS.health + 'doseQuantity', qty));
151
+ }
152
+
153
+ if (resource.route) {
154
+ const routeText = codeableConceptText(resource.route);
155
+ if (routeText) quads.push(tripleStr(subjectUri, NS.health + 'route', routeText));
156
+ }
157
+
158
+ if (resource.site) {
159
+ const siteText = codeableConceptText(resource.site);
160
+ if (siteText) quads.push(tripleStr(subjectUri, NS.health + 'site', siteText));
161
+ }
162
+
163
+ if (Array.isArray(resource.performer) && resource.performer.length > 0) {
164
+ const performer = resource.performer[0]?.actor?.display;
165
+ if (performer) quads.push(tripleStr(subjectUri, NS.health + 'administeringProvider', performer));
166
+ }
167
+
168
+ if (resource.location?.display) {
169
+ quads.push(tripleStr(subjectUri, NS.health + 'administeringLocation', resource.location.display));
170
+ }
171
+
172
+ if (resource.note && Array.isArray(resource.note)) {
173
+ const noteText = resource.note.map((n: any) => n.text).filter(Boolean).join('; ');
174
+ if (noteText) quads.push(tripleStr(subjectUri, NS.health + 'notes', noteText));
175
+ }
176
+
177
+ if (resource.id) {
178
+ quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
179
+ }
180
+
181
+ return {
182
+ turtle: '',
183
+ jsonld: quadsToJsonLd(quads, 'health:ImmunizationRecord'),
184
+ warnings,
185
+ resourceType: 'Immunization',
186
+ cascadeType: 'health:ImmunizationRecord',
187
+ _quads: quads,
188
+ };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Coverage converter
193
+ // ---------------------------------------------------------------------------
194
+
195
+ export function convertCoverage(resource: any): ConversionResult & { _quads: Quad[] } {
196
+ const warnings: string[] = [];
197
+ const subjectUri = `urn:uuid:${randomUUID()}`;
198
+ const quads: Quad[] = [];
199
+
200
+ quads.push(tripleType(subjectUri, NS.coverage + 'InsurancePlan'));
201
+ quads.push(...commonTriples(subjectUri));
202
+
203
+ if (Array.isArray(resource.payor) && resource.payor.length > 0) {
204
+ const payorName = resource.payor[0]?.display ?? 'Unknown Insurance';
205
+ quads.push(tripleStr(subjectUri, NS.coverage + 'providerName', payorName));
206
+ } else {
207
+ quads.push(tripleStr(subjectUri, NS.coverage + 'providerName', 'Unknown Insurance'));
208
+ warnings.push('No payor information found in Coverage resource');
209
+ }
210
+
211
+ if (resource.subscriberId) {
212
+ quads.push(tripleStr(subjectUri, NS.coverage + 'memberId', resource.subscriberId));
213
+ quads.push(tripleStr(subjectUri, NS.coverage + 'subscriberId', resource.subscriberId));
214
+ } else if (resource.identifier && Array.isArray(resource.identifier) && resource.identifier.length > 0) {
215
+ const memberId = resource.identifier[0]?.value ?? '';
216
+ quads.push(tripleStr(subjectUri, NS.coverage + 'memberId', memberId));
217
+ } else {
218
+ warnings.push('No member/subscriber ID found in Coverage resource');
219
+ }
220
+
221
+ if (resource.type) {
222
+ const typeText = resource.type.coding?.[0]?.code ?? codeableConceptText(resource.type) ?? 'primary';
223
+ quads.push(tripleStr(subjectUri, NS.coverage + 'coverageType', typeText));
224
+ } else {
225
+ quads.push(tripleStr(subjectUri, NS.coverage + 'coverageType', 'primary'));
226
+ }
227
+
228
+ if (Array.isArray(resource.class)) {
229
+ for (const cls of resource.class) {
230
+ const clsType = cls.type?.coding?.[0]?.code ?? '';
231
+ if (clsType === 'group' && cls.value) {
232
+ quads.push(tripleStr(subjectUri, NS.coverage + 'groupNumber', cls.value));
233
+ if (cls.name) quads.push(tripleStr(subjectUri, NS.coverage + 'planName', cls.name));
234
+ } else if (clsType === 'plan' && cls.value) {
235
+ quads.push(tripleStr(subjectUri, NS.coverage + 'planName', cls.name ?? cls.value));
236
+ } else if (clsType === 'rxbin' && cls.value) {
237
+ quads.push(tripleStr(subjectUri, NS.coverage + 'rxBin', cls.value));
238
+ } else if (clsType === 'rxpcn' && cls.value) {
239
+ quads.push(tripleStr(subjectUri, NS.coverage + 'rxPcn', cls.value));
240
+ } else if (clsType === 'rxgroup' && cls.value) {
241
+ quads.push(tripleStr(subjectUri, NS.coverage + 'rxGroup', cls.value));
242
+ }
243
+ }
244
+ }
245
+
246
+ if (resource.relationship) {
247
+ const relCode = resource.relationship.coding?.[0]?.code ?? 'self';
248
+ quads.push(tripleStr(subjectUri, NS.coverage + 'subscriberRelationship', relCode));
249
+ }
250
+
251
+ if (resource.period?.start) {
252
+ quads.push(tripleDate(subjectUri, NS.coverage + 'effectiveStart', resource.period.start.substring(0, 10)));
253
+ }
254
+ if (resource.period?.end) {
255
+ quads.push(tripleDate(subjectUri, NS.coverage + 'effectiveEnd', resource.period.end.substring(0, 10)));
256
+ }
257
+
258
+ if (resource.id) {
259
+ quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
260
+ }
261
+
262
+ return {
263
+ turtle: '',
264
+ jsonld: quadsToJsonLd(quads, 'coverage:InsurancePlan'),
265
+ warnings,
266
+ resourceType: 'Coverage',
267
+ cascadeType: 'coverage:InsurancePlan',
268
+ _quads: quads,
269
+ };
270
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * FHIR -> Cascade dispatcher and public API.
3
+ *
4
+ * Routes FHIR resources to the appropriate per-type converter and
5
+ * provides the main public conversion functions.
6
+ *
7
+ * Individual converters are in:
8
+ * - converters-clinical.ts Medications, conditions, allergies, observations
9
+ * - converters-demographics.ts Patient, immunization, coverage
10
+ */
11
+
12
+ import type { Quad } from 'n3';
13
+
14
+ import { type ConversionResult, quadsToTurtle } from './types.js';
15
+
16
+ import {
17
+ convertMedicationStatement,
18
+ convertCondition,
19
+ convertAllergyIntolerance,
20
+ isVitalSignObservation,
21
+ convertObservationLab,
22
+ convertObservationVital,
23
+ } from './converters-clinical.js';
24
+
25
+ import {
26
+ convertPatient,
27
+ convertImmunization,
28
+ convertCoverage,
29
+ } from './converters-demographics.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Main dispatcher: single FHIR resource -> Cascade
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export function convertFhirResourceToQuads(fhirResource: any): (ConversionResult & { _quads: Quad[] }) | null {
36
+ const resourceType = fhirResource?.resourceType as string | undefined;
37
+ if (!resourceType) return null;
38
+
39
+ switch (resourceType) {
40
+ case 'MedicationStatement':
41
+ case 'MedicationRequest':
42
+ return convertMedicationStatement(fhirResource);
43
+ case 'Condition':
44
+ return convertCondition(fhirResource);
45
+ case 'AllergyIntolerance':
46
+ return convertAllergyIntolerance(fhirResource);
47
+ case 'Observation':
48
+ if (isVitalSignObservation(fhirResource)) {
49
+ return convertObservationVital(fhirResource);
50
+ }
51
+ return convertObservationLab(fhirResource);
52
+ case 'Patient':
53
+ return convertPatient(fhirResource);
54
+ case 'Immunization':
55
+ return convertImmunization(fhirResource);
56
+ case 'Coverage':
57
+ return convertCoverage(fhirResource);
58
+ default:
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export async function convertFhirToCascade(fhirResource: any): Promise<ConversionResult> {
64
+ const result = convertFhirResourceToQuads(fhirResource);
65
+ if (!result) {
66
+ return {
67
+ turtle: '',
68
+ warnings: [`Unsupported FHIR resource type: ${fhirResource?.resourceType ?? 'unknown'}`],
69
+ resourceType: fhirResource?.resourceType ?? 'unknown',
70
+ cascadeType: 'unknown',
71
+ };
72
+ }
73
+
74
+ const turtle = await quadsToTurtle(result._quads);
75
+ return {
76
+ turtle,
77
+ jsonld: result.jsonld,
78
+ warnings: result.warnings,
79
+ resourceType: result.resourceType,
80
+ cascadeType: result.cascadeType,
81
+ };
82
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * FHIR conversion utilities.
3
+ *
4
+ * Converts between FHIR R4 JSON and Cascade Protocol RDF (Turtle/JSON-LD).
5
+ *
6
+ * Supported FHIR R4 resource types:
7
+ * - MedicationStatement / MedicationRequest -> health:MedicationRecord
8
+ * - Condition -> health:ConditionRecord
9
+ * - AllergyIntolerance -> health:AllergyRecord
10
+ * - Observation (lab) -> health:LabResultRecord
11
+ * - Observation (vital) -> clinical:VitalSign
12
+ * - Patient -> cascade:PatientProfile
13
+ * - Immunization -> health:ImmunizationRecord
14
+ * - Coverage -> coverage:InsurancePlan
15
+ *
16
+ * Zero network calls. All conversion is local.
17
+ *
18
+ * This module re-exports all public API for backward compatibility.
19
+ * Internal implementation is split across:
20
+ * - types.ts Shared types, namespaces, and quad helpers
21
+ * - fhir-to-cascade.ts FHIR -> Cascade converters
22
+ * - cascade-to-fhir.ts Cascade -> FHIR converters
23
+ */
24
+
25
+ import type { Quad } from 'n3';
26
+
27
+ import {
28
+ type InputFormat,
29
+ type OutputFormat,
30
+ type ConversionResult,
31
+ type BatchConversionResult,
32
+ SUPPORTED_TYPES,
33
+ quadsToTurtle,
34
+ quadsToJsonLd,
35
+ } from './types.js';
36
+
37
+ import { convertFhirResourceToQuads } from './fhir-to-cascade.js';
38
+ import { convertCascadeToFhir } from './cascade-to-fhir.js';
39
+
40
+ // Re-export public types
41
+ export type { InputFormat, OutputFormat, ConversionResult, BatchConversionResult };
42
+
43
+ // Re-export public functions from sub-modules
44
+ export { convertFhirResourceToQuads, convertFhirToCascade } from './fhir-to-cascade.js';
45
+ export { convertCascadeToFhir } from './cascade-to-fhir.js';
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Batch conversion (FHIR Bundle support)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Convert an entire FHIR input (single resource or Bundle) to Cascade format.
53
+ */
54
+ export async function convert(
55
+ input: string,
56
+ from: InputFormat,
57
+ to: OutputFormat,
58
+ outputSerialization: 'turtle' | 'jsonld' = 'turtle',
59
+ ): Promise<BatchConversionResult> {
60
+ const warnings: string[] = [];
61
+ const errors: string[] = [];
62
+ const results: ConversionResult[] = [];
63
+
64
+ if (from === 'fhir' && (to === 'cascade' || to === 'turtle' || to === 'jsonld')) {
65
+ // FHIR -> Cascade
66
+ let parsed: any;
67
+ try {
68
+ parsed = JSON.parse(input);
69
+ } catch {
70
+ return {
71
+ success: false, output: '', format: to, resourceCount: 0,
72
+ warnings: [], errors: ['Invalid JSON input'], results: [],
73
+ };
74
+ }
75
+
76
+ // Collect resources from Bundle or single resource
77
+ const fhirResources: any[] = [];
78
+ if (parsed.resourceType === 'Bundle' && Array.isArray(parsed.entry)) {
79
+ for (const entry of parsed.entry) {
80
+ if (entry.resource) fhirResources.push(entry.resource);
81
+ }
82
+ } else if (parsed.resourceType) {
83
+ fhirResources.push(parsed);
84
+ } else {
85
+ return {
86
+ success: false, output: '', format: to, resourceCount: 0,
87
+ warnings: [], errors: ['Input does not appear to be a FHIR resource or Bundle'], results: [],
88
+ };
89
+ }
90
+
91
+ const allQuads: Quad[] = [];
92
+ for (const res of fhirResources) {
93
+ if (!SUPPORTED_TYPES.has(res.resourceType)) {
94
+ warnings.push(`Skipping unsupported FHIR resource type: ${res.resourceType}`);
95
+ continue;
96
+ }
97
+ const result = convertFhirResourceToQuads(res);
98
+ if (result) {
99
+ allQuads.push(...result._quads);
100
+ results.push({
101
+ turtle: '', // will be filled with combined output
102
+ jsonld: result.jsonld,
103
+ warnings: result.warnings,
104
+ resourceType: result.resourceType,
105
+ cascadeType: result.cascadeType,
106
+ });
107
+ warnings.push(...result.warnings);
108
+ }
109
+ }
110
+
111
+ if (allQuads.length === 0) {
112
+ return {
113
+ success: false, output: '', format: to, resourceCount: 0,
114
+ warnings, errors: ['No convertible FHIR resources found'], results: [],
115
+ };
116
+ }
117
+
118
+ // Determine output format
119
+ let output: string;
120
+ if (outputSerialization === 'jsonld' || to === 'jsonld') {
121
+ const jsonLd = quadsToJsonLd(allQuads, results[0]?.cascadeType ?? '');
122
+ output = JSON.stringify(jsonLd, null, 2);
123
+ } else {
124
+ output = await quadsToTurtle(allQuads);
125
+ }
126
+
127
+ return {
128
+ success: true,
129
+ output,
130
+ format: to === 'cascade' ? (outputSerialization === 'jsonld' ? 'jsonld' : 'turtle') : to,
131
+ resourceCount: results.length,
132
+ warnings,
133
+ errors,
134
+ results,
135
+ };
136
+ } else if (from === 'cascade' && to === 'fhir') {
137
+ // Cascade -> FHIR
138
+ const { resources, warnings: convWarnings } = await convertCascadeToFhir(input);
139
+ warnings.push(...convWarnings);
140
+
141
+ if (resources.length === 0) {
142
+ return {
143
+ success: false, output: '', format: 'fhir', resourceCount: 0,
144
+ warnings, errors: ['No resources converted from Cascade Turtle'], results: [],
145
+ };
146
+ }
147
+
148
+ const output = resources.length === 1
149
+ ? JSON.stringify(resources[0], null, 2)
150
+ : JSON.stringify({ resourceType: 'Bundle', type: 'collection', entry: resources.map(r => ({ resource: r })) }, null, 2);
151
+
152
+ return {
153
+ success: true,
154
+ output,
155
+ format: 'fhir',
156
+ resourceCount: resources.length,
157
+ warnings,
158
+ errors,
159
+ results: resources.map(r => ({
160
+ turtle: '',
161
+ warnings: [],
162
+ resourceType: r.resourceType,
163
+ cascadeType: 'fhir',
164
+ })),
165
+ };
166
+ } else if (from === 'c-cda') {
167
+ return {
168
+ success: false, output: '', format: to, resourceCount: 0,
169
+ warnings: [], errors: ['C-CDA conversion is not yet supported'], results: [],
170
+ };
171
+ } else {
172
+ return {
173
+ success: false, output: '', format: to, resourceCount: 0,
174
+ warnings: [], errors: [`Unsupported conversion: ${from} -> ${to}`], results: [],
175
+ };
176
+ }
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Format detection
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /**
184
+ * Detect the format of input data by inspecting its content.
185
+ */
186
+ export function detectFormat(input: string): InputFormat | null {
187
+ const trimmed = input.trim();
188
+
189
+ // Check for FHIR JSON (has "resourceType")
190
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
191
+ try {
192
+ const parsed = JSON.parse(trimmed);
193
+ if (parsed.resourceType) return 'fhir';
194
+ if (parsed['@context'] || parsed['@type']) return 'cascade'; // JSON-LD
195
+ } catch {
196
+ // Not valid JSON
197
+ }
198
+ }
199
+
200
+ // Check for Turtle (has @prefix declarations or common Cascade namespace URIs)
201
+ if (
202
+ trimmed.includes('@prefix') ||
203
+ trimmed.includes('ns.cascadeprotocol.org') ||
204
+ /^<[^>]+>\s+a\s+/.test(trimmed)
205
+ ) {
206
+ return 'cascade';
207
+ }
208
+
209
+ // Check for C-CDA (XML with ClinicalDocument root)
210
+ if (trimmed.startsWith('<?xml') || trimmed.includes('<ClinicalDocument')) {
211
+ return 'c-cda';
212
+ }
213
+
214
+ return null;
215
+ }