@the-cascade-protocol/cli 0.2.0 → 0.2.1

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 (50) hide show
  1. package/dist/commands/pod/helpers.d.ts +1 -1
  2. package/dist/commands/pod/helpers.d.ts.map +1 -1
  3. package/dist/commands/pod/helpers.js +5 -20
  4. package/dist/commands/pod/helpers.js.map +1 -1
  5. package/package.json +17 -5
  6. package/.dockerignore +0 -7
  7. package/.eslintrc.json +0 -23
  8. package/.prettierrc +0 -7
  9. package/Dockerfile +0 -18
  10. package/src/commands/capabilities.ts +0 -235
  11. package/src/commands/conformance.ts +0 -447
  12. package/src/commands/convert.ts +0 -164
  13. package/src/commands/pod/export.ts +0 -85
  14. package/src/commands/pod/helpers.ts +0 -449
  15. package/src/commands/pod/index.ts +0 -32
  16. package/src/commands/pod/info.ts +0 -239
  17. package/src/commands/pod/init.ts +0 -273
  18. package/src/commands/pod/query.ts +0 -224
  19. package/src/commands/serve.ts +0 -92
  20. package/src/commands/validate.ts +0 -303
  21. package/src/index.ts +0 -58
  22. package/src/lib/fhir-converter/cascade-to-fhir.ts +0 -369
  23. package/src/lib/fhir-converter/converters-clinical.ts +0 -446
  24. package/src/lib/fhir-converter/converters-demographics.ts +0 -270
  25. package/src/lib/fhir-converter/fhir-to-cascade.ts +0 -82
  26. package/src/lib/fhir-converter/index.ts +0 -215
  27. package/src/lib/fhir-converter/types.ts +0 -318
  28. package/src/lib/mcp/audit.ts +0 -107
  29. package/src/lib/mcp/server.ts +0 -192
  30. package/src/lib/mcp/tools.ts +0 -668
  31. package/src/lib/output.ts +0 -76
  32. package/src/lib/shacl-validator.ts +0 -314
  33. package/src/lib/turtle-parser.ts +0 -277
  34. package/src/shapes/checkup.shapes.ttl +0 -1459
  35. package/src/shapes/clinical.shapes.ttl +0 -1350
  36. package/src/shapes/clinical.ttl +0 -1369
  37. package/src/shapes/core.shapes.ttl +0 -450
  38. package/src/shapes/core.ttl +0 -603
  39. package/src/shapes/coverage.shapes.ttl +0 -214
  40. package/src/shapes/coverage.ttl +0 -182
  41. package/src/shapes/health.shapes.ttl +0 -697
  42. package/src/shapes/health.ttl +0 -859
  43. package/src/shapes/pots.shapes.ttl +0 -481
  44. package/test-fixtures/fhir-bundle-example.json +0 -216
  45. package/test-fixtures/fhir-medication-example.json +0 -18
  46. package/tests/cli.test.ts +0 -126
  47. package/tests/fhir-converter.test.ts +0 -874
  48. package/tests/mcp-server.test.ts +0 -396
  49. package/tests/pod.test.ts +0 -400
  50. package/tsconfig.json +0 -24
@@ -1,270 +0,0 @@
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
- }
@@ -1,82 +0,0 @@
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
- }
@@ -1,215 +0,0 @@
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
- }