@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,446 +0,0 @@
1
- /**
2
- * FHIR -> Cascade converters for clinical record types.
3
- *
4
- * Converts:
5
- * - MedicationStatement / MedicationRequest -> health:MedicationRecord
6
- * - Condition -> health:ConditionRecord
7
- * - AllergyIntolerance -> health:AllergyRecord
8
- * - Observation (lab) -> health:LabResultRecord
9
- * - Observation (vital) -> clinical:VitalSign
10
- */
11
-
12
- import { randomUUID } from 'node:crypto';
13
- import type { Quad } from 'n3';
14
-
15
- import {
16
- type ConversionResult,
17
- NS,
18
- CODING_SYSTEM_MAP,
19
- VITAL_LOINC_CODES,
20
- VITAL_CATEGORIES,
21
- extractCodings,
22
- codeableConceptText,
23
- tripleStr,
24
- tripleBool,
25
- tripleDouble,
26
- tripleRef,
27
- tripleType,
28
- tripleDateTime,
29
- commonTriples,
30
- quadsToJsonLd,
31
- } from './types.js';
32
-
33
- // ---------------------------------------------------------------------------
34
- // Medication converter
35
- // ---------------------------------------------------------------------------
36
-
37
- export function convertMedicationStatement(resource: any): ConversionResult & { _quads: Quad[] } {
38
- const warnings: string[] = [];
39
- const subjectUri = `urn:uuid:${randomUUID()}`;
40
- const quads: Quad[] = [];
41
-
42
- quads.push(tripleType(subjectUri, NS.health + 'MedicationRecord'));
43
- quads.push(...commonTriples(subjectUri));
44
-
45
- // Medication name
46
- const medName = codeableConceptText(resource.medicationCodeableConcept)
47
- ?? resource.medicationReference?.display
48
- ?? 'Unknown Medication';
49
- quads.push(tripleStr(subjectUri, NS.health + 'medicationName', medName));
50
-
51
- // isActive from FHIR status
52
- const status = resource.status as string | undefined;
53
- const isActive = status === 'active' || status === 'intended' || status === 'on-hold';
54
- quads.push(tripleBool(subjectUri, NS.health + 'isActive', isActive));
55
-
56
- // Drug codes
57
- const codings = extractCodings(resource.medicationCodeableConcept);
58
- for (const coding of codings) {
59
- const nsUri = CODING_SYSTEM_MAP[coding.system];
60
- if (nsUri) {
61
- quads.push(tripleRef(subjectUri, NS.clinical + 'drugCode', nsUri + coding.code));
62
- if (nsUri === NS.rxnorm) {
63
- quads.push(tripleRef(subjectUri, NS.health + 'rxNormCode', nsUri + coding.code));
64
- }
65
- } else {
66
- warnings.push(`Unknown coding system: ${coding.system} (code ${coding.code})`);
67
- }
68
- }
69
-
70
- // Dosage
71
- const dosage = Array.isArray(resource.dosage) ? resource.dosage[0] : undefined;
72
- if (dosage) {
73
- if (dosage.text) {
74
- quads.push(tripleStr(subjectUri, NS.health + 'dose', dosage.text));
75
- }
76
- if (dosage.route?.text) {
77
- quads.push(tripleStr(subjectUri, NS.health + 'route', dosage.route.text));
78
- } else if (dosage.route?.coding?.[0]?.display) {
79
- quads.push(tripleStr(subjectUri, NS.health + 'route', dosage.route.coding[0].display));
80
- }
81
- if (dosage.timing?.repeat?.frequency) {
82
- const freq = dosage.timing.repeat.frequency;
83
- const periodUnit = dosage.timing.repeat.periodUnit ?? 'd';
84
- const unitLabel = periodUnit === 'd' ? 'daily' : periodUnit === 'wk' ? 'weekly' : periodUnit;
85
- const freqText = freq === 1 ? `once ${unitLabel}` : `${freq} times ${unitLabel}`;
86
- quads.push(tripleStr(subjectUri, NS.health + 'frequency', freqText));
87
- }
88
- }
89
-
90
- // Effective period
91
- if (resource.effectivePeriod?.start) {
92
- quads.push(tripleDateTime(subjectUri, NS.health + 'startDate', resource.effectivePeriod.start));
93
- } else if (resource.effectiveDateTime) {
94
- quads.push(tripleDateTime(subjectUri, NS.health + 'startDate', resource.effectiveDateTime));
95
- }
96
- if (resource.effectivePeriod?.end) {
97
- quads.push(tripleDateTime(subjectUri, NS.health + 'endDate', resource.effectivePeriod.end));
98
- }
99
-
100
- // Provenance class -- based on resource type
101
- const fhirResourceType = resource.resourceType as string;
102
- if (fhirResourceType === 'MedicationStatement') {
103
- quads.push(tripleStr(subjectUri, NS.clinical + 'sourceFhirResourceType', 'MedicationStatement'));
104
- quads.push(tripleStr(subjectUri, NS.clinical + 'clinicalIntent', 'reportedUse'));
105
- } else if (fhirResourceType === 'MedicationRequest') {
106
- quads.push(tripleStr(subjectUri, NS.clinical + 'sourceFhirResourceType', 'MedicationRequest'));
107
- quads.push(tripleStr(subjectUri, NS.clinical + 'clinicalIntent', 'prescribed'));
108
- }
109
- quads.push(tripleStr(subjectUri, NS.clinical + 'provenanceClass', 'imported'));
110
-
111
- if (resource.id) {
112
- quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
113
- }
114
-
115
- if (resource.note && Array.isArray(resource.note)) {
116
- const noteText = resource.note.map((n: any) => n.text).filter(Boolean).join('; ');
117
- if (noteText) quads.push(tripleStr(subjectUri, NS.health + 'notes', noteText));
118
- }
119
-
120
- return {
121
- turtle: '',
122
- warnings,
123
- resourceType: fhirResourceType,
124
- cascadeType: 'health:MedicationRecord',
125
- jsonld: quadsToJsonLd(quads, 'health:MedicationRecord'),
126
- _quads: quads,
127
- } as ConversionResult & { _quads: Quad[] };
128
- }
129
-
130
- // ---------------------------------------------------------------------------
131
- // Condition converter
132
- // ---------------------------------------------------------------------------
133
-
134
- export function convertCondition(resource: any): ConversionResult & { _quads: Quad[] } {
135
- const warnings: string[] = [];
136
- const subjectUri = `urn:uuid:${randomUUID()}`;
137
- const quads: Quad[] = [];
138
-
139
- quads.push(tripleType(subjectUri, NS.health + 'ConditionRecord'));
140
- quads.push(...commonTriples(subjectUri));
141
-
142
- const condName = codeableConceptText(resource.code) ?? 'Unknown Condition';
143
- quads.push(tripleStr(subjectUri, NS.health + 'conditionName', condName));
144
-
145
- const clinicalStatus = resource.clinicalStatus?.coding?.[0]?.code ?? 'active';
146
- quads.push(tripleStr(subjectUri, NS.health + 'status', clinicalStatus));
147
-
148
- if (resource.onsetDateTime) {
149
- quads.push(tripleDateTime(subjectUri, NS.health + 'onsetDate', resource.onsetDateTime));
150
- } else if (resource.onsetPeriod?.start) {
151
- quads.push(tripleDateTime(subjectUri, NS.health + 'onsetDate', resource.onsetPeriod.start));
152
- }
153
-
154
- if (resource.abatementDateTime) {
155
- quads.push(tripleDateTime(subjectUri, NS.health + 'abatementDate', resource.abatementDateTime));
156
- }
157
-
158
- const codings = extractCodings(resource.code);
159
- for (const coding of codings) {
160
- const nsUri = CODING_SYSTEM_MAP[coding.system];
161
- if (nsUri === NS.icd10) {
162
- quads.push(tripleRef(subjectUri, NS.health + 'icd10Code', nsUri + coding.code));
163
- } else if (nsUri === NS.sct) {
164
- quads.push(tripleRef(subjectUri, NS.health + 'snomedCode', nsUri + coding.code));
165
- } else if (nsUri) {
166
- warnings.push(`Condition code from non-standard system: ${coding.system}`);
167
- }
168
- }
169
-
170
- if (resource.id) {
171
- quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
172
- }
173
-
174
- if (resource.note && Array.isArray(resource.note)) {
175
- const noteText = resource.note.map((n: any) => n.text).filter(Boolean).join('; ');
176
- if (noteText) quads.push(tripleStr(subjectUri, NS.health + 'notes', noteText));
177
- }
178
-
179
- return {
180
- turtle: '',
181
- jsonld: quadsToJsonLd(quads, 'health:ConditionRecord'),
182
- warnings,
183
- resourceType: 'Condition',
184
- cascadeType: 'health:ConditionRecord',
185
- _quads: quads,
186
- };
187
- }
188
-
189
- // ---------------------------------------------------------------------------
190
- // AllergyIntolerance converter
191
- // ---------------------------------------------------------------------------
192
-
193
- export function convertAllergyIntolerance(resource: any): ConversionResult & { _quads: Quad[] } {
194
- const warnings: string[] = [];
195
- const subjectUri = `urn:uuid:${randomUUID()}`;
196
- const quads: Quad[] = [];
197
-
198
- quads.push(tripleType(subjectUri, NS.health + 'AllergyRecord'));
199
- quads.push(...commonTriples(subjectUri));
200
-
201
- const allergen = codeableConceptText(resource.code) ?? 'Unknown Allergen';
202
- quads.push(tripleStr(subjectUri, NS.health + 'allergen', allergen));
203
-
204
- if (Array.isArray(resource.category) && resource.category.length > 0) {
205
- quads.push(tripleStr(subjectUri, NS.health + 'allergyCategory', resource.category[0]));
206
- }
207
-
208
- if (Array.isArray(resource.reaction) && resource.reaction.length > 0) {
209
- const manifestations = resource.reaction
210
- .flatMap((r: any) => r.manifestation ?? [])
211
- .map((m: any) => codeableConceptText(m))
212
- .filter(Boolean);
213
- if (manifestations.length > 0) {
214
- quads.push(tripleStr(subjectUri, NS.health + 'reaction', manifestations.join(', ')));
215
- }
216
- const severity = resource.reaction[0]?.severity;
217
- if (severity) {
218
- const severityMap: Record<string, string> = { mild: 'mild', moderate: 'moderate', severe: 'severe' };
219
- quads.push(tripleStr(subjectUri, NS.health + 'allergySeverity', severityMap[severity] ?? severity));
220
- }
221
- }
222
-
223
- if (resource.criticality && !(Array.isArray(resource.reaction) && resource.reaction[0]?.severity)) {
224
- const critMap: Record<string, string> = { low: 'mild', high: 'severe', 'unable-to-assess': 'moderate' };
225
- quads.push(tripleStr(subjectUri, NS.health + 'allergySeverity', critMap[resource.criticality] ?? resource.criticality));
226
- }
227
-
228
- if (resource.onsetDateTime) {
229
- quads.push(tripleDateTime(subjectUri, NS.health + 'onsetDate', resource.onsetDateTime));
230
- }
231
-
232
- if (resource.id) {
233
- quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
234
- }
235
-
236
- if (resource.note && Array.isArray(resource.note)) {
237
- const noteText = resource.note.map((n: any) => n.text).filter(Boolean).join('; ');
238
- if (noteText) quads.push(tripleStr(subjectUri, NS.health + 'notes', noteText));
239
- }
240
-
241
- return {
242
- turtle: '',
243
- jsonld: quadsToJsonLd(quads, 'health:AllergyRecord'),
244
- warnings,
245
- resourceType: 'AllergyIntolerance',
246
- cascadeType: 'health:AllergyRecord',
247
- _quads: quads,
248
- };
249
- }
250
-
251
- // ---------------------------------------------------------------------------
252
- // Observation: vital sign detection
253
- // ---------------------------------------------------------------------------
254
-
255
- export function isVitalSignObservation(resource: any): boolean {
256
- if (Array.isArray(resource.category)) {
257
- for (const cat of resource.category) {
258
- if (Array.isArray(cat.coding)) {
259
- for (const c of cat.coding) {
260
- if (VITAL_CATEGORIES.includes(c.code)) return true;
261
- }
262
- }
263
- }
264
- }
265
- const codings = extractCodings(resource.code);
266
- for (const c of codings) {
267
- if (c.system === 'http://loinc.org' && VITAL_LOINC_CODES[c.code]) return true;
268
- }
269
- return false;
270
- }
271
-
272
- // ---------------------------------------------------------------------------
273
- // Observation (lab) converter
274
- // ---------------------------------------------------------------------------
275
-
276
- export function convertObservationLab(resource: any): ConversionResult & { _quads: Quad[] } {
277
- const warnings: string[] = [];
278
- const subjectUri = `urn:uuid:${randomUUID()}`;
279
- const quads: Quad[] = [];
280
-
281
- quads.push(tripleType(subjectUri, NS.health + 'LabResultRecord'));
282
- quads.push(...commonTriples(subjectUri));
283
-
284
- const testName = codeableConceptText(resource.code) ?? 'Unknown Lab Test';
285
- quads.push(tripleStr(subjectUri, NS.health + 'testName', testName));
286
-
287
- if (resource.valueQuantity) {
288
- quads.push(tripleStr(subjectUri, NS.health + 'resultValue', String(resource.valueQuantity.value)));
289
- if (resource.valueQuantity.unit) {
290
- quads.push(tripleStr(subjectUri, NS.health + 'resultUnit', resource.valueQuantity.unit));
291
- }
292
- } else if (resource.valueString) {
293
- quads.push(tripleStr(subjectUri, NS.health + 'resultValue', resource.valueString));
294
- } else if (resource.valueCodeableConcept) {
295
- const valText = codeableConceptText(resource.valueCodeableConcept) ?? '';
296
- quads.push(tripleStr(subjectUri, NS.health + 'resultValue', valText));
297
- } else {
298
- quads.push(tripleStr(subjectUri, NS.health + 'resultValue', ''));
299
- warnings.push('No result value found in Observation resource');
300
- }
301
-
302
- if (resource.interpretation && Array.isArray(resource.interpretation) && resource.interpretation.length > 0) {
303
- const interpCode = resource.interpretation[0]?.coding?.[0]?.code ?? 'unknown';
304
- const interpMap: Record<string, string> = {
305
- N: 'normal', H: 'abnormal', L: 'abnormal', A: 'abnormal',
306
- HH: 'critical', LL: 'critical', AA: 'critical',
307
- HU: 'critical', LU: 'critical',
308
- };
309
- quads.push(tripleStr(subjectUri, NS.health + 'interpretation', interpMap[interpCode] ?? 'unknown'));
310
- } else {
311
- quads.push(tripleStr(subjectUri, NS.health + 'interpretation', 'unknown'));
312
- }
313
-
314
- const effectiveDate = resource.effectiveDateTime ?? resource.effectivePeriod?.start ?? resource.issued;
315
- if (effectiveDate) {
316
- quads.push(tripleDateTime(subjectUri, NS.health + 'performedDate', effectiveDate));
317
- } else {
318
- warnings.push('No effective date found in Observation resource');
319
- }
320
-
321
- const codings = extractCodings(resource.code);
322
- for (const c of codings) {
323
- if (c.system === 'http://loinc.org') {
324
- quads.push(tripleRef(subjectUri, NS.health + 'testCode', NS.loinc + c.code));
325
- }
326
- }
327
-
328
- if (Array.isArray(resource.category)) {
329
- for (const cat of resource.category) {
330
- if (Array.isArray(cat.coding)) {
331
- for (const c of cat.coding) {
332
- if (c.code && c.code !== 'laboratory') {
333
- quads.push(tripleStr(subjectUri, NS.health + 'labCategory', c.code));
334
- }
335
- }
336
- }
337
- if (cat.text) {
338
- quads.push(tripleStr(subjectUri, NS.health + 'labCategory', cat.text));
339
- }
340
- }
341
- }
342
-
343
- if (Array.isArray(resource.referenceRange) && resource.referenceRange.length > 0) {
344
- const rr = resource.referenceRange[0];
345
- const parts: string[] = [];
346
- if (rr.low?.value !== undefined) parts.push(String(rr.low.value));
347
- if (rr.high?.value !== undefined) parts.push(String(rr.high.value));
348
- const unit = rr.low?.unit ?? rr.high?.unit ?? '';
349
- if (parts.length === 2) {
350
- quads.push(tripleStr(subjectUri, NS.health + 'referenceRange', `${parts[0]}-${parts[1]} ${unit}`.trim()));
351
- } else if (rr.text) {
352
- quads.push(tripleStr(subjectUri, NS.health + 'referenceRange', rr.text));
353
- }
354
- }
355
-
356
- if (resource.id) {
357
- quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
358
- }
359
-
360
- return {
361
- turtle: '',
362
- jsonld: quadsToJsonLd(quads, 'health:LabResultRecord'),
363
- warnings,
364
- resourceType: 'Observation',
365
- cascadeType: 'health:LabResultRecord',
366
- _quads: quads,
367
- };
368
- }
369
-
370
- // ---------------------------------------------------------------------------
371
- // Observation (vital sign) converter
372
- // ---------------------------------------------------------------------------
373
-
374
- export function convertObservationVital(resource: any): ConversionResult & { _quads: Quad[] } {
375
- const warnings: string[] = [];
376
- const subjectUri = `urn:uuid:${randomUUID()}`;
377
- const quads: Quad[] = [];
378
-
379
- quads.push(tripleType(subjectUri, NS.clinical + 'VitalSign'));
380
- quads.push(...commonTriples(subjectUri));
381
-
382
- const codings = extractCodings(resource.code);
383
- let vitalInfo: { type: string; name: string; unit: string; snomedCode: string } | undefined;
384
- for (const c of codings) {
385
- if (c.system === 'http://loinc.org' && VITAL_LOINC_CODES[c.code]) {
386
- vitalInfo = VITAL_LOINC_CODES[c.code];
387
- quads.push(tripleRef(subjectUri, NS.clinical + 'loincCode', NS.loinc + c.code));
388
- break;
389
- }
390
- }
391
-
392
- if (vitalInfo) {
393
- quads.push(tripleStr(subjectUri, NS.clinical + 'vitalType', vitalInfo.type));
394
- quads.push(tripleStr(subjectUri, NS.clinical + 'vitalTypeName', vitalInfo.name));
395
- quads.push(tripleRef(subjectUri, NS.clinical + 'snomedCode', NS.sct + vitalInfo.snomedCode));
396
- } else {
397
- const name = codeableConceptText(resource.code) ?? 'Unknown Vital';
398
- quads.push(tripleStr(subjectUri, NS.clinical + 'vitalType', name.toLowerCase().replace(/\s+/g, '_')));
399
- quads.push(tripleStr(subjectUri, NS.clinical + 'vitalTypeName', name));
400
- warnings.push(`Unknown vital sign LOINC code -- using display name: ${name}`);
401
- }
402
-
403
- if (resource.valueQuantity) {
404
- quads.push(tripleDouble(subjectUri, NS.clinical + 'value', resource.valueQuantity.value));
405
- quads.push(tripleStr(subjectUri, NS.clinical + 'unit', resource.valueQuantity.unit ?? vitalInfo?.unit ?? ''));
406
- } else {
407
- warnings.push('No valueQuantity found in vital sign Observation');
408
- }
409
-
410
- const effectiveDate = resource.effectiveDateTime ?? resource.effectivePeriod?.start;
411
- if (effectiveDate) {
412
- quads.push(tripleDateTime(subjectUri, NS.clinical + 'effectiveDate', effectiveDate));
413
- }
414
-
415
- if (Array.isArray(resource.referenceRange) && resource.referenceRange.length > 0) {
416
- const rr = resource.referenceRange[0];
417
- if (rr.low?.value !== undefined) {
418
- quads.push(tripleDouble(subjectUri, NS.clinical + 'referenceRangeLow', rr.low.value));
419
- }
420
- if (rr.high?.value !== undefined) {
421
- quads.push(tripleDouble(subjectUri, NS.clinical + 'referenceRangeHigh', rr.high.value));
422
- }
423
- }
424
-
425
- if (resource.interpretation && Array.isArray(resource.interpretation) && resource.interpretation.length > 0) {
426
- const interpCode = resource.interpretation[0]?.coding?.[0]?.code ?? 'unknown';
427
- const interpMap: Record<string, string> = {
428
- N: 'normal', H: 'high', L: 'low', A: 'abnormal',
429
- HH: 'critical', LL: 'critical',
430
- };
431
- quads.push(tripleStr(subjectUri, NS.clinical + 'interpretation', interpMap[interpCode] ?? interpCode));
432
- }
433
-
434
- if (resource.id) {
435
- quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
436
- }
437
-
438
- return {
439
- turtle: '',
440
- jsonld: quadsToJsonLd(quads, 'clinical:VitalSign'),
441
- warnings,
442
- resourceType: 'Observation',
443
- cascadeType: 'clinical:VitalSign',
444
- _quads: quads,
445
- };
446
- }