@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,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
- }