@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.
- package/dist/commands/pod/helpers.d.ts +1 -1
- package/dist/commands/pod/helpers.d.ts.map +1 -1
- package/dist/commands/pod/helpers.js +5 -20
- package/dist/commands/pod/helpers.js.map +1 -1
- package/package.json +17 -5
- package/.dockerignore +0 -7
- package/.eslintrc.json +0 -23
- package/.prettierrc +0 -7
- package/Dockerfile +0 -18
- package/src/commands/capabilities.ts +0 -235
- package/src/commands/conformance.ts +0 -447
- package/src/commands/convert.ts +0 -164
- package/src/commands/pod/export.ts +0 -85
- package/src/commands/pod/helpers.ts +0 -449
- package/src/commands/pod/index.ts +0 -32
- package/src/commands/pod/info.ts +0 -239
- package/src/commands/pod/init.ts +0 -273
- package/src/commands/pod/query.ts +0 -224
- package/src/commands/serve.ts +0 -92
- package/src/commands/validate.ts +0 -303
- package/src/index.ts +0 -58
- package/src/lib/fhir-converter/cascade-to-fhir.ts +0 -369
- package/src/lib/fhir-converter/converters-clinical.ts +0 -446
- package/src/lib/fhir-converter/converters-demographics.ts +0 -270
- package/src/lib/fhir-converter/fhir-to-cascade.ts +0 -82
- package/src/lib/fhir-converter/index.ts +0 -215
- package/src/lib/fhir-converter/types.ts +0 -318
- package/src/lib/mcp/audit.ts +0 -107
- package/src/lib/mcp/server.ts +0 -192
- package/src/lib/mcp/tools.ts +0 -668
- package/src/lib/output.ts +0 -76
- package/src/lib/shacl-validator.ts +0 -314
- package/src/lib/turtle-parser.ts +0 -277
- package/src/shapes/checkup.shapes.ttl +0 -1459
- package/src/shapes/clinical.shapes.ttl +0 -1350
- package/src/shapes/clinical.ttl +0 -1369
- package/src/shapes/core.shapes.ttl +0 -450
- package/src/shapes/core.ttl +0 -603
- package/src/shapes/coverage.shapes.ttl +0 -214
- package/src/shapes/coverage.ttl +0 -182
- package/src/shapes/health.shapes.ttl +0 -697
- package/src/shapes/health.ttl +0 -859
- package/src/shapes/pots.shapes.ttl +0 -481
- package/test-fixtures/fhir-bundle-example.json +0 -216
- package/test-fixtures/fhir-medication-example.json +0 -18
- package/tests/cli.test.ts +0 -126
- package/tests/fhir-converter.test.ts +0 -874
- package/tests/mcp-server.test.ts +0 -396
- package/tests/pod.test.ts +0 -400
- package/tsconfig.json +0 -24
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cascade -> FHIR (reverse conversion).
|
|
3
|
-
*
|
|
4
|
-
* Parses Cascade Protocol Turtle using n3, identifies the resource type from
|
|
5
|
-
* rdf:type, and maps Cascade predicates back to FHIR R4 fields.
|
|
6
|
-
*
|
|
7
|
-
* Not all Cascade fields have FHIR equivalents -- lost fields are reported
|
|
8
|
-
* as warnings.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Parser, type Quad } from 'n3';
|
|
12
|
-
import { NS } from './types.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Convert Cascade Turtle to FHIR R4 JSON.
|
|
16
|
-
*/
|
|
17
|
-
export async function convertCascadeToFhir(turtle: string): Promise<{
|
|
18
|
-
resources: any[];
|
|
19
|
-
warnings: string[];
|
|
20
|
-
}> {
|
|
21
|
-
const warnings: string[] = [];
|
|
22
|
-
const resources: any[] = [];
|
|
23
|
-
|
|
24
|
-
// Parse Turtle
|
|
25
|
-
const parser = new Parser();
|
|
26
|
-
const quads: Quad[] = [];
|
|
27
|
-
try {
|
|
28
|
-
const parsed = parser.parse(turtle);
|
|
29
|
-
quads.push(...parsed);
|
|
30
|
-
} catch (err: any) {
|
|
31
|
-
return { resources: [], warnings: [`Turtle parse error: ${err.message}`] };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Group quads by subject
|
|
35
|
-
const subjects = new Map<string, Quad[]>();
|
|
36
|
-
for (const q of quads) {
|
|
37
|
-
const subj = q.subject.value;
|
|
38
|
-
if (!subjects.has(subj)) subjects.set(subj, []);
|
|
39
|
-
subjects.get(subj)!.push(q);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const [_subjectUri, subjectQuads] of subjects) {
|
|
43
|
-
// Find rdf:type
|
|
44
|
-
const typeQuad = subjectQuads.find(q => q.predicate.value === NS.rdf + 'type');
|
|
45
|
-
if (!typeQuad) continue;
|
|
46
|
-
|
|
47
|
-
const rdfType = typeQuad.object.value;
|
|
48
|
-
|
|
49
|
-
// Build a predicate->value map for quick access
|
|
50
|
-
const pv = new Map<string, string[]>();
|
|
51
|
-
for (const q of subjectQuads) {
|
|
52
|
-
const pred = q.predicate.value;
|
|
53
|
-
if (!pv.has(pred)) pv.set(pred, []);
|
|
54
|
-
pv.get(pred)!.push(q.object.value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const getFirst = (pred: string): string | undefined => pv.get(pred)?.[0];
|
|
58
|
-
|
|
59
|
-
if (rdfType === NS.health + 'MedicationRecord') {
|
|
60
|
-
const fhirResource: any = {
|
|
61
|
-
resourceType: 'MedicationStatement',
|
|
62
|
-
status: 'active',
|
|
63
|
-
medicationCodeableConcept: { text: getFirst(NS.health + 'medicationName') ?? '' },
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// isActive -> status
|
|
67
|
-
const isActive = getFirst(NS.health + 'isActive');
|
|
68
|
-
if (isActive === 'false') fhirResource.status = 'stopped';
|
|
69
|
-
|
|
70
|
-
// Drug codes
|
|
71
|
-
const drugCodes = pv.get(NS.clinical + 'drugCode') ?? [];
|
|
72
|
-
const codingArr: any[] = [];
|
|
73
|
-
for (const uri of drugCodes) {
|
|
74
|
-
if (uri.startsWith(NS.rxnorm)) {
|
|
75
|
-
codingArr.push({ system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: uri.slice(NS.rxnorm.length) });
|
|
76
|
-
} else if (uri.startsWith(NS.sct)) {
|
|
77
|
-
codingArr.push({ system: 'http://snomed.info/sct', code: uri.slice(NS.sct.length) });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (codingArr.length > 0) {
|
|
81
|
-
fhirResource.medicationCodeableConcept.coding = codingArr;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Dosage
|
|
85
|
-
const doseText = getFirst(NS.health + 'dose');
|
|
86
|
-
if (doseText) {
|
|
87
|
-
fhirResource.dosage = [{ text: doseText }];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Dates
|
|
91
|
-
const startDate = getFirst(NS.health + 'startDate');
|
|
92
|
-
const endDate = getFirst(NS.health + 'endDate');
|
|
93
|
-
if (startDate || endDate) {
|
|
94
|
-
fhirResource.effectivePeriod = {};
|
|
95
|
-
if (startDate) fhirResource.effectivePeriod.start = startDate;
|
|
96
|
-
if (endDate) fhirResource.effectivePeriod.end = endDate;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Source record ID
|
|
100
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
101
|
-
if (srcId) fhirResource.id = srcId;
|
|
102
|
-
|
|
103
|
-
// Cascade-only fields that have no FHIR equivalent
|
|
104
|
-
const cascadeOnlyFields = [
|
|
105
|
-
NS.clinical + 'provenanceClass',
|
|
106
|
-
NS.clinical + 'clinicalIntent',
|
|
107
|
-
NS.cascade + 'schemaVersion',
|
|
108
|
-
NS.health + 'medicationClass',
|
|
109
|
-
];
|
|
110
|
-
for (const field of cascadeOnlyFields) {
|
|
111
|
-
if (getFirst(field)) {
|
|
112
|
-
const shortName = field.split('#')[1] ?? field;
|
|
113
|
-
warnings.push(`Cascade field '${shortName}' has no FHIR equivalent and was not included in output`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
resources.push(fhirResource);
|
|
118
|
-
} else if (rdfType === NS.health + 'ConditionRecord') {
|
|
119
|
-
const fhirResource: any = {
|
|
120
|
-
resourceType: 'Condition',
|
|
121
|
-
code: {
|
|
122
|
-
text: getFirst(NS.health + 'conditionName') ?? '',
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Status
|
|
127
|
-
const status = getFirst(NS.health + 'status');
|
|
128
|
-
if (status) {
|
|
129
|
-
fhirResource.clinicalStatus = {
|
|
130
|
-
coding: [{ system: 'http://terminology.hl7.org/CodeSystem/condition-clinical', code: status }],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Onset
|
|
135
|
-
const onset = getFirst(NS.health + 'onsetDate');
|
|
136
|
-
if (onset) fhirResource.onsetDateTime = onset;
|
|
137
|
-
|
|
138
|
-
// Codes
|
|
139
|
-
const codingArr: any[] = [];
|
|
140
|
-
const icd10 = pv.get(NS.health + 'icd10Code') ?? [];
|
|
141
|
-
for (const uri of icd10) {
|
|
142
|
-
codingArr.push({ system: 'http://hl7.org/fhir/sid/icd-10-cm', code: uri.startsWith(NS.icd10) ? uri.slice(NS.icd10.length) : uri });
|
|
143
|
-
}
|
|
144
|
-
const snomed = pv.get(NS.health + 'snomedCode') ?? [];
|
|
145
|
-
for (const uri of snomed) {
|
|
146
|
-
codingArr.push({ system: 'http://snomed.info/sct', code: uri.startsWith(NS.sct) ? uri.slice(NS.sct.length) : uri });
|
|
147
|
-
}
|
|
148
|
-
if (codingArr.length > 0) fhirResource.code.coding = codingArr;
|
|
149
|
-
|
|
150
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
151
|
-
if (srcId) fhirResource.id = srcId;
|
|
152
|
-
|
|
153
|
-
resources.push(fhirResource);
|
|
154
|
-
} else if (rdfType === NS.health + 'AllergyRecord') {
|
|
155
|
-
const fhirResource: any = {
|
|
156
|
-
resourceType: 'AllergyIntolerance',
|
|
157
|
-
code: { text: getFirst(NS.health + 'allergen') ?? '' },
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const cat = getFirst(NS.health + 'allergyCategory');
|
|
161
|
-
if (cat) fhirResource.category = [cat];
|
|
162
|
-
|
|
163
|
-
const severity = getFirst(NS.health + 'allergySeverity');
|
|
164
|
-
const reaction = getFirst(NS.health + 'reaction');
|
|
165
|
-
if (reaction || severity) {
|
|
166
|
-
const rxn: any = {};
|
|
167
|
-
if (reaction) rxn.manifestation = [{ text: reaction }];
|
|
168
|
-
if (severity) rxn.severity = severity;
|
|
169
|
-
fhirResource.reaction = [rxn];
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const onset = getFirst(NS.health + 'onsetDate');
|
|
173
|
-
if (onset) fhirResource.onsetDateTime = onset;
|
|
174
|
-
|
|
175
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
176
|
-
if (srcId) fhirResource.id = srcId;
|
|
177
|
-
|
|
178
|
-
resources.push(fhirResource);
|
|
179
|
-
} else if (rdfType === NS.health + 'LabResultRecord') {
|
|
180
|
-
const fhirResource: any = {
|
|
181
|
-
resourceType: 'Observation',
|
|
182
|
-
code: { text: getFirst(NS.health + 'testName') ?? '' },
|
|
183
|
-
category: [{ coding: [{ code: 'laboratory' }] }],
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// LOINC code
|
|
187
|
-
const testCode = pv.get(NS.health + 'testCode') ?? [];
|
|
188
|
-
const codingArr: any[] = [];
|
|
189
|
-
for (const uri of testCode) {
|
|
190
|
-
const code = uri.startsWith(NS.loinc) ? uri.slice(NS.loinc.length) : uri;
|
|
191
|
-
codingArr.push({ system: 'http://loinc.org', code });
|
|
192
|
-
}
|
|
193
|
-
if (codingArr.length > 0) fhirResource.code.coding = codingArr;
|
|
194
|
-
|
|
195
|
-
// Value
|
|
196
|
-
const resultVal = getFirst(NS.health + 'resultValue');
|
|
197
|
-
const resultUnit = getFirst(NS.health + 'resultUnit');
|
|
198
|
-
if (resultVal) {
|
|
199
|
-
const numVal = parseFloat(resultVal);
|
|
200
|
-
if (!isNaN(numVal)) {
|
|
201
|
-
fhirResource.valueQuantity = { value: numVal };
|
|
202
|
-
if (resultUnit) fhirResource.valueQuantity.unit = resultUnit;
|
|
203
|
-
} else {
|
|
204
|
-
fhirResource.valueString = resultVal;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Date
|
|
209
|
-
const perfDate = getFirst(NS.health + 'performedDate');
|
|
210
|
-
if (perfDate) fhirResource.effectiveDateTime = perfDate;
|
|
211
|
-
|
|
212
|
-
// Interpretation
|
|
213
|
-
const interp = getFirst(NS.health + 'interpretation');
|
|
214
|
-
if (interp) {
|
|
215
|
-
const revInterpMap: Record<string, string> = {
|
|
216
|
-
normal: 'N', abnormal: 'A', critical: 'HH', unknown: 'UNK',
|
|
217
|
-
};
|
|
218
|
-
fhirResource.interpretation = [{
|
|
219
|
-
coding: [{ code: revInterpMap[interp] ?? interp }],
|
|
220
|
-
}];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
224
|
-
if (srcId) fhirResource.id = srcId;
|
|
225
|
-
|
|
226
|
-
resources.push(fhirResource);
|
|
227
|
-
} else if (rdfType === NS.clinical + 'VitalSign') {
|
|
228
|
-
const fhirResource: any = {
|
|
229
|
-
resourceType: 'Observation',
|
|
230
|
-
code: {},
|
|
231
|
-
category: [{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'vital-signs' }] }],
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// LOINC code
|
|
235
|
-
const loincUri = getFirst(NS.clinical + 'loincCode');
|
|
236
|
-
if (loincUri) {
|
|
237
|
-
const code = loincUri.startsWith(NS.loinc) ? loincUri.slice(NS.loinc.length) : loincUri;
|
|
238
|
-
fhirResource.code.coding = [{ system: 'http://loinc.org', code }];
|
|
239
|
-
}
|
|
240
|
-
const vitalName = getFirst(NS.clinical + 'vitalTypeName');
|
|
241
|
-
if (vitalName) fhirResource.code.text = vitalName;
|
|
242
|
-
|
|
243
|
-
// Value
|
|
244
|
-
const value = getFirst(NS.clinical + 'value');
|
|
245
|
-
const unit = getFirst(NS.clinical + 'unit');
|
|
246
|
-
if (value) {
|
|
247
|
-
fhirResource.valueQuantity = { value: parseFloat(value) };
|
|
248
|
-
if (unit) fhirResource.valueQuantity.unit = unit;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Date
|
|
252
|
-
const effDate = getFirst(NS.clinical + 'effectiveDate');
|
|
253
|
-
if (effDate) fhirResource.effectiveDateTime = effDate;
|
|
254
|
-
|
|
255
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
256
|
-
if (srcId) fhirResource.id = srcId;
|
|
257
|
-
|
|
258
|
-
// Warn about Cascade-specific fields
|
|
259
|
-
if (getFirst(NS.clinical + 'snomedCode')) {
|
|
260
|
-
warnings.push("Cascade field 'snomedCode' has no standard FHIR Observation field and was not included");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
resources.push(fhirResource);
|
|
264
|
-
} else if (rdfType === NS.cascade + 'PatientProfile') {
|
|
265
|
-
const fhirResource: any = {
|
|
266
|
-
resourceType: 'Patient',
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const dob = getFirst(NS.cascade + 'dateOfBirth');
|
|
270
|
-
if (dob) fhirResource.birthDate = dob;
|
|
271
|
-
|
|
272
|
-
const sex = getFirst(NS.cascade + 'biologicalSex');
|
|
273
|
-
if (sex) {
|
|
274
|
-
const sexMap: Record<string, string> = { male: 'male', female: 'female', intersex: 'other' };
|
|
275
|
-
fhirResource.gender = sexMap[sex] ?? 'unknown';
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const marital = getFirst(NS.cascade + 'maritalStatus');
|
|
279
|
-
if (marital) {
|
|
280
|
-
const maritalMap: Record<string, string> = {
|
|
281
|
-
single: 'S', married: 'M', divorced: 'D', widowed: 'W',
|
|
282
|
-
separated: 'A', domestic_partnership: 'T',
|
|
283
|
-
};
|
|
284
|
-
fhirResource.maritalStatus = {
|
|
285
|
-
coding: [{ code: maritalMap[marital] ?? 'UNK' }],
|
|
286
|
-
text: marital,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const profileId = getFirst(NS.cascade + 'profileId');
|
|
291
|
-
if (profileId) fhirResource.id = profileId;
|
|
292
|
-
|
|
293
|
-
// Warn about Cascade-only fields
|
|
294
|
-
const cascadeOnly = ['computedAge', 'ageGroup', 'genderIdentity'];
|
|
295
|
-
for (const field of cascadeOnly) {
|
|
296
|
-
if (getFirst(NS.cascade + field)) {
|
|
297
|
-
warnings.push(`Cascade field '${field}' has no FHIR Patient equivalent and was not included`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
resources.push(fhirResource);
|
|
302
|
-
} else if (rdfType === NS.health + 'ImmunizationRecord') {
|
|
303
|
-
const fhirResource: any = {
|
|
304
|
-
resourceType: 'Immunization',
|
|
305
|
-
status: getFirst(NS.health + 'status') ?? 'completed',
|
|
306
|
-
vaccineCode: { text: getFirst(NS.health + 'vaccineName') ?? '' },
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
const adminDate = getFirst(NS.health + 'administrationDate');
|
|
310
|
-
if (adminDate) fhirResource.occurrenceDateTime = adminDate;
|
|
311
|
-
|
|
312
|
-
const vaccineCode = getFirst(NS.health + 'vaccineCode');
|
|
313
|
-
if (vaccineCode) {
|
|
314
|
-
// Strip "CVX-" prefix
|
|
315
|
-
const code = vaccineCode.startsWith('CVX-') ? vaccineCode.slice(4) : vaccineCode;
|
|
316
|
-
fhirResource.vaccineCode.coding = [{ system: 'http://hl7.org/fhir/sid/cvx', code }];
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const manufacturer = getFirst(NS.health + 'manufacturer');
|
|
320
|
-
if (manufacturer) fhirResource.manufacturer = { display: manufacturer };
|
|
321
|
-
|
|
322
|
-
const lotNumber = getFirst(NS.health + 'lotNumber');
|
|
323
|
-
if (lotNumber) fhirResource.lotNumber = lotNumber;
|
|
324
|
-
|
|
325
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
326
|
-
if (srcId) fhirResource.id = srcId;
|
|
327
|
-
|
|
328
|
-
resources.push(fhirResource);
|
|
329
|
-
} else if (rdfType === NS.coverage + 'InsurancePlan') {
|
|
330
|
-
const fhirResource: any = {
|
|
331
|
-
resourceType: 'Coverage',
|
|
332
|
-
status: 'active',
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const providerName = getFirst(NS.coverage + 'providerName');
|
|
336
|
-
if (providerName) fhirResource.payor = [{ display: providerName }];
|
|
337
|
-
|
|
338
|
-
const memberId = getFirst(NS.coverage + 'memberId');
|
|
339
|
-
if (memberId) fhirResource.subscriberId = memberId;
|
|
340
|
-
|
|
341
|
-
const groupNum = getFirst(NS.coverage + 'groupNumber');
|
|
342
|
-
const planName = getFirst(NS.coverage + 'planName');
|
|
343
|
-
const classArr: any[] = [];
|
|
344
|
-
if (groupNum) classArr.push({ type: { coding: [{ code: 'group' }] }, value: groupNum, name: planName });
|
|
345
|
-
else if (planName) classArr.push({ type: { coding: [{ code: 'plan' }] }, value: planName });
|
|
346
|
-
if (classArr.length > 0) fhirResource.class = classArr;
|
|
347
|
-
|
|
348
|
-
const start = getFirst(NS.coverage + 'effectiveStart');
|
|
349
|
-
const end = getFirst(NS.coverage + 'effectiveEnd');
|
|
350
|
-
if (start || end) {
|
|
351
|
-
fhirResource.period = {};
|
|
352
|
-
if (start) fhirResource.period.start = start;
|
|
353
|
-
if (end) fhirResource.period.end = end;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const rel = getFirst(NS.coverage + 'subscriberRelationship');
|
|
357
|
-
if (rel) fhirResource.relationship = { coding: [{ code: rel }] };
|
|
358
|
-
|
|
359
|
-
const srcId = getFirst(NS.health + 'sourceRecordId');
|
|
360
|
-
if (srcId) fhirResource.id = srcId;
|
|
361
|
-
|
|
362
|
-
resources.push(fhirResource);
|
|
363
|
-
} else {
|
|
364
|
-
warnings.push(`Unknown Cascade RDF type: ${rdfType}`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return { resources, warnings };
|
|
369
|
-
}
|