@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
package/tests/pod.test.ts DELETED
@@ -1,400 +0,0 @@
1
- /**
2
- * Unit tests for pod command modules.
3
- *
4
- * Tests pod helpers (parsePod, extractLabel, etc.), pod init directory
5
- * structure creation, pod info record counting and provenance detection,
6
- * pod query filtering, and pod export (ZIP and directory copy).
7
- */
8
-
9
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
- import { execSync } from 'child_process';
11
- import * as fs from 'fs/promises';
12
- import * as path from 'path';
13
- import { resolve } from 'path';
14
- import { existsSync } from 'fs';
15
-
16
- const CLI_PATH = resolve(__dirname, '../dist/index.js');
17
- const REFERENCE_POD = resolve(__dirname, '../../reference-patient-pod');
18
-
19
- function runCli(args: string): string {
20
- try {
21
- return execSync(`node ${CLI_PATH} ${args}`, {
22
- encoding: 'utf-8',
23
- timeout: 30000,
24
- }).trim();
25
- } catch (error: unknown) {
26
- const execError = error as { stdout?: string; stderr?: string; status?: number };
27
- return (execError.stdout ?? '').trim() + (execError.stderr ?? '').trim();
28
- }
29
- }
30
-
31
- // =============================================================================
32
- // Tests: Pod helpers (unit tests of pure functions)
33
- // =============================================================================
34
-
35
- import {
36
- DATA_TYPES,
37
- resolvePodDir,
38
- normalizeProvenanceLabel,
39
- extractLabelFromProps,
40
- selectKeyProperties,
41
- } from '../src/commands/pod/helpers.js';
42
-
43
- describe('Pod helpers', () => {
44
- describe('DATA_TYPES registry', () => {
45
- it('should contain all expected data types', () => {
46
- const expectedTypes = [
47
- 'medications', 'conditions', 'allergies', 'lab-results',
48
- 'immunizations', 'vital-signs', 'insurance', 'patient-profile',
49
- 'heart-rate', 'blood-pressure', 'activity', 'sleep', 'supplements',
50
- ];
51
- for (const t of expectedTypes) {
52
- expect(DATA_TYPES[t]).toBeDefined();
53
- expect(DATA_TYPES[t].label).toBeTruthy();
54
- expect(DATA_TYPES[t].directory).toMatch(/^(clinical|wellness)$/);
55
- expect(DATA_TYPES[t].filename).toMatch(/\.ttl$/);
56
- }
57
- });
58
-
59
- it('should classify clinical vs wellness correctly', () => {
60
- expect(DATA_TYPES['medications'].directory).toBe('clinical');
61
- expect(DATA_TYPES['conditions'].directory).toBe('clinical');
62
- expect(DATA_TYPES['heart-rate'].directory).toBe('wellness');
63
- expect(DATA_TYPES['sleep'].directory).toBe('wellness');
64
- });
65
- });
66
-
67
- describe('resolvePodDir', () => {
68
- it('should resolve relative paths against cwd', () => {
69
- const result = resolvePodDir('my-pod');
70
- expect(result).toBe(path.resolve(process.cwd(), 'my-pod'));
71
- });
72
-
73
- it('should return absolute paths as-is', () => {
74
- const result = resolvePodDir('/tmp/my-pod');
75
- expect(result).toBe('/tmp/my-pod');
76
- });
77
- });
78
-
79
- describe('normalizeProvenanceLabel', () => {
80
- it('should convert core: prefix to cascade:', () => {
81
- expect(normalizeProvenanceLabel('core:ClinicalGenerated')).toBe('cascade:ClinicalGenerated');
82
- });
83
-
84
- it('should leave other prefixes unchanged', () => {
85
- expect(normalizeProvenanceLabel('cascade:DeviceGenerated')).toBe('cascade:DeviceGenerated');
86
- expect(normalizeProvenanceLabel('prov:Activity')).toBe('prov:Activity');
87
- });
88
- });
89
-
90
- describe('extractLabelFromProps', () => {
91
- it('should extract medication name', () => {
92
- const props = { 'health:medicationName': 'Metformin', 'health:dose': '500mg' };
93
- expect(extractLabelFromProps(props)).toBe('Metformin');
94
- });
95
-
96
- it('should extract condition name', () => {
97
- expect(extractLabelFromProps({ 'health:conditionName': 'Diabetes' })).toBe('Diabetes');
98
- });
99
-
100
- it('should extract allergen', () => {
101
- expect(extractLabelFromProps({ 'health:allergen': 'Penicillin' })).toBe('Penicillin');
102
- });
103
-
104
- it('should return undefined when no label key is found', () => {
105
- expect(extractLabelFromProps({ 'health:someOtherProp': 'value' })).toBeUndefined();
106
- });
107
-
108
- it('should prefer medicationName over other keys', () => {
109
- const props = {
110
- 'health:medicationName': 'Metformin',
111
- 'foaf:name': 'Should not be chosen',
112
- };
113
- expect(extractLabelFromProps(props)).toBe('Metformin');
114
- });
115
- });
116
-
117
- describe('selectKeyProperties', () => {
118
- it('should select medication-specific properties', () => {
119
- const props = {
120
- 'health:dose': '500mg',
121
- 'health:frequency': 'twice daily',
122
- 'health:route': 'oral',
123
- 'cascade:schemaVersion': '1.3',
124
- 'health:medicationName': 'Metformin',
125
- };
126
- const result = selectKeyProperties('medications', props);
127
- expect(result['health:dose']).toBe('500mg');
128
- expect(result['health:frequency']).toBe('twice daily');
129
- expect(result['cascade:schemaVersion']).toBe('1.3');
130
- });
131
-
132
- it('should select condition-specific properties', () => {
133
- const props = {
134
- 'health:status': 'active',
135
- 'health:icd10Code': 'E11.9',
136
- 'cascade:dataProvenance': 'cascade:ClinicalGenerated',
137
- };
138
- const result = selectKeyProperties('conditions', props);
139
- expect(result['health:status']).toBe('active');
140
- expect(result['health:icd10Code']).toBe('E11.9');
141
- expect(result['cascade:dataProvenance']).toBe('cascade:ClinicalGenerated');
142
- });
143
-
144
- it('should show first few properties for unknown type', () => {
145
- const props = {
146
- 'custom:fieldA': 'A',
147
- 'custom:fieldB': 'B',
148
- 'custom:fieldC': 'C',
149
- };
150
- const result = selectKeyProperties('unknownType', props);
151
- expect(Object.keys(result).length).toBeGreaterThan(0);
152
- expect(Object.keys(result).length).toBeLessThanOrEqual(5);
153
- });
154
- });
155
- });
156
-
157
- // =============================================================================
158
- // Tests: Pod init
159
- // =============================================================================
160
-
161
- describe('pod init', () => {
162
- let tempDir: string;
163
-
164
- beforeEach(async () => {
165
- tempDir = path.join('/tmp', `cascade-test-init-${Date.now()}-${Math.random().toString(36).slice(2)}`);
166
- });
167
-
168
- afterEach(async () => {
169
- try {
170
- await fs.rm(tempDir, { recursive: true, force: true });
171
- } catch {
172
- // ignore cleanup errors
173
- }
174
- });
175
-
176
- it('should create the standard directory structure', () => {
177
- const podDir = path.join(tempDir, 'my-pod');
178
- runCli(`pod init ${podDir}`);
179
-
180
- expect(existsSync(path.join(podDir, '.well-known', 'solid'))).toBe(true);
181
- expect(existsSync(path.join(podDir, 'profile', 'card.ttl'))).toBe(true);
182
- expect(existsSync(path.join(podDir, 'settings', 'publicTypeIndex.ttl'))).toBe(true);
183
- expect(existsSync(path.join(podDir, 'settings', 'privateTypeIndex.ttl'))).toBe(true);
184
- expect(existsSync(path.join(podDir, 'clinical'))).toBe(true);
185
- expect(existsSync(path.join(podDir, 'wellness'))).toBe(true);
186
- expect(existsSync(path.join(podDir, 'index.ttl'))).toBe(true);
187
- expect(existsSync(path.join(podDir, 'README.md'))).toBe(true);
188
- });
189
-
190
- it('should produce valid JSON in .well-known/solid', async () => {
191
- const podDir = path.join(tempDir, 'json-pod');
192
- runCli(`pod init ${podDir}`);
193
-
194
- const solidJson = await fs.readFile(path.join(podDir, '.well-known', 'solid'), 'utf-8');
195
- const parsed = JSON.parse(solidJson);
196
- expect(parsed.version).toBe('1.0');
197
- expect(parsed.profile).toContain('card.ttl');
198
- expect(parsed.publicTypeIndex).toContain('publicTypeIndex.ttl');
199
- });
200
-
201
- it('should include Turtle prefixes in profile/card.ttl', async () => {
202
- const podDir = path.join(tempDir, 'prefix-pod');
203
- runCli(`pod init ${podDir}`);
204
-
205
- const profileContent = await fs.readFile(path.join(podDir, 'profile', 'card.ttl'), 'utf-8');
206
- expect(profileContent).toContain('@prefix cascade:');
207
- expect(profileContent).toContain('@prefix foaf:');
208
- expect(profileContent).toContain('schemaVersion');
209
- });
210
-
211
- it('should output JSON when --json flag is used', () => {
212
- const podDir = path.join(tempDir, 'json-out-pod');
213
- const output = runCli(`--json pod init ${podDir}`);
214
- const parsed = JSON.parse(output);
215
- expect(parsed.status).toBe('created');
216
- expect(parsed.files).toBeInstanceOf(Array);
217
- expect(parsed.files.length).toBeGreaterThan(0);
218
- });
219
-
220
- it('should error when directory already has a pod', () => {
221
- const podDir = path.join(tempDir, 'double-init');
222
- runCli(`pod init ${podDir}`);
223
- const output = runCli(`pod init ${podDir}`);
224
- expect(output).toContain('already contains');
225
- });
226
- });
227
-
228
- // =============================================================================
229
- // Tests: Pod info (using reference patient pod)
230
- // =============================================================================
231
-
232
- describe('pod info', () => {
233
- it('should output JSON with data summary', () => {
234
- const output = runCli(`--json pod info ${REFERENCE_POD}`);
235
- const parsed = JSON.parse(output);
236
-
237
- expect(parsed.clinical).toBeInstanceOf(Array);
238
- expect(parsed.wellness).toBeInstanceOf(Array);
239
- expect(parsed.provenanceSources).toBeInstanceOf(Array);
240
- });
241
-
242
- it('should detect record counts for clinical data', () => {
243
- const output = runCli(`--json pod info ${REFERENCE_POD}`);
244
- const parsed = JSON.parse(output);
245
-
246
- // The reference pod has medications, conditions, allergies, etc.
247
- const medEntry = parsed.clinical.find((c: any) => c.file === 'medications.ttl');
248
- if (medEntry) {
249
- expect(medEntry.records).toBeGreaterThan(0);
250
- }
251
-
252
- const condEntry = parsed.clinical.find((c: any) => c.file === 'conditions.ttl');
253
- if (condEntry) {
254
- expect(condEntry.records).toBeGreaterThan(0);
255
- }
256
- });
257
-
258
- it('should detect provenance information', () => {
259
- const output = runCli(`--json pod info ${REFERENCE_POD}`);
260
- const parsed = JSON.parse(output);
261
- expect(parsed.provenanceSources.length).toBeGreaterThan(0);
262
- });
263
-
264
- it('should include schema version', () => {
265
- const output = runCli(`--json pod info ${REFERENCE_POD}`);
266
- const parsed = JSON.parse(output);
267
- // Schema version may come from patient profile or index
268
- expect(parsed.schemaVersion).toBeTruthy();
269
- });
270
-
271
- it('should show patient name when profile exists', () => {
272
- const output = runCli(`--json pod info ${REFERENCE_POD}`);
273
- const parsed = JSON.parse(output);
274
- // The reference pod should have a patient name
275
- expect(parsed.patient).toBeDefined();
276
- expect(parsed.patient.name).toBeTruthy();
277
- });
278
-
279
- it('should error for non-existent pod directory', () => {
280
- const output = runCli(`--json pod info /tmp/nonexistent-pod-xyz`);
281
- expect(output).toContain('not found');
282
- });
283
-
284
- it('should produce human-readable output without --json', () => {
285
- const output = runCli(`pod info ${REFERENCE_POD}`);
286
- expect(output).toContain('Cascade Pod');
287
- });
288
- });
289
-
290
- // =============================================================================
291
- // Tests: Pod query (using reference patient pod)
292
- // =============================================================================
293
-
294
- describe('pod query', () => {
295
- it('should query medications and return records', () => {
296
- const output = runCli(`--json pod query ${REFERENCE_POD} --medications`);
297
- const parsed = JSON.parse(output);
298
- expect(parsed.dataTypes).toBeDefined();
299
- if (parsed.dataTypes.medications) {
300
- expect(parsed.dataTypes.medications.count).toBeGreaterThan(0);
301
- expect(parsed.dataTypes.medications.records).toBeInstanceOf(Array);
302
- }
303
- });
304
-
305
- it('should query conditions', () => {
306
- const output = runCli(`--json pod query ${REFERENCE_POD} --conditions`);
307
- const parsed = JSON.parse(output);
308
- if (parsed.dataTypes.conditions) {
309
- expect(parsed.dataTypes.conditions.count).toBeGreaterThan(0);
310
- }
311
- });
312
-
313
- it('should query allergies', () => {
314
- const output = runCli(`--json pod query ${REFERENCE_POD} --allergies`);
315
- const parsed = JSON.parse(output);
316
- if (parsed.dataTypes.allergies) {
317
- expect(parsed.dataTypes.allergies.count).toBeGreaterThan(0);
318
- }
319
- });
320
-
321
- it('should query all data types with --all flag', () => {
322
- const output = runCli(`--json pod query ${REFERENCE_POD} --all`);
323
- const parsed = JSON.parse(output);
324
- expect(parsed.dataTypes).toBeDefined();
325
- // Should have multiple data type keys
326
- expect(Object.keys(parsed.dataTypes).length).toBeGreaterThan(1);
327
- });
328
-
329
- it('should include record properties in query results', () => {
330
- const output = runCli(`--json pod query ${REFERENCE_POD} --medications`);
331
- const parsed = JSON.parse(output);
332
- if (parsed.dataTypes.medications && parsed.dataTypes.medications.records.length > 0) {
333
- const firstRecord = parsed.dataTypes.medications.records[0];
334
- expect(firstRecord.id).toBeTruthy();
335
- expect(firstRecord.type).toBeTruthy();
336
- expect(firstRecord.properties).toBeDefined();
337
- }
338
- });
339
-
340
- it('should error when no filter is specified', () => {
341
- const output = runCli(`pod query ${REFERENCE_POD}`);
342
- expect(output).toContain('No query filter');
343
- });
344
-
345
- it('should error for non-existent pod directory', () => {
346
- const output = runCli(`pod query /tmp/nonexistent-pod-xyz --all`);
347
- expect(output).toContain('not found');
348
- });
349
- });
350
-
351
- // =============================================================================
352
- // Tests: Pod export
353
- // =============================================================================
354
-
355
- describe('pod export', () => {
356
- let tempExportDir: string;
357
-
358
- beforeEach(async () => {
359
- tempExportDir = path.join('/tmp', `cascade-test-export-${Date.now()}-${Math.random().toString(36).slice(2)}`);
360
- await fs.mkdir(tempExportDir, { recursive: true });
361
- });
362
-
363
- afterEach(async () => {
364
- try {
365
- await fs.rm(tempExportDir, { recursive: true, force: true });
366
- } catch {
367
- // ignore cleanup errors
368
- }
369
- });
370
-
371
- it('should export pod as ZIP archive', () => {
372
- const zipPath = path.join(tempExportDir, 'test-export.zip');
373
- const output = runCli(`--json pod export ${REFERENCE_POD} --format zip --output ${zipPath}`);
374
- const parsed = JSON.parse(output);
375
- expect(parsed.status).toBe('exported');
376
- expect(parsed.format).toBe('zip');
377
- expect(existsSync(zipPath)).toBe(true);
378
- });
379
-
380
- it('should export pod as directory copy', () => {
381
- const destDir = path.join(tempExportDir, 'pod-copy');
382
- const output = runCli(`--json pod export ${REFERENCE_POD} --format directory --output ${destDir}`);
383
- const parsed = JSON.parse(output);
384
- expect(parsed.status).toBe('exported');
385
- expect(parsed.format).toBe('directory');
386
- expect(existsSync(path.join(destDir, 'index.ttl'))).toBe(true);
387
- expect(existsSync(path.join(destDir, 'clinical'))).toBe(true);
388
- expect(existsSync(path.join(destDir, 'wellness'))).toBe(true);
389
- });
390
-
391
- it('should error for non-existent pod directory', () => {
392
- const output = runCli(`pod export /tmp/nonexistent-pod-xyz --format zip`);
393
- expect(output).toContain('not found');
394
- });
395
-
396
- it('should error for unknown export format', () => {
397
- const output = runCli(`pod export ${REFERENCE_POD} --format csv`);
398
- expect(output).toContain('Unknown export format');
399
- });
400
- });
package/tsconfig.json DELETED
@@ -1,24 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "lib": ["ES2022"],
7
- "outDir": "./dist",
8
- "rootDir": "./src",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "resolveJsonModule": true,
14
- "declaration": true,
15
- "declarationMap": true,
16
- "sourceMap": true,
17
- "noUnusedLocals": true,
18
- "noUnusedParameters": true,
19
- "noImplicitReturns": true,
20
- "noFallthroughCasesInSwitch": true
21
- },
22
- "include": ["src/**/*.ts"],
23
- "exclude": ["node_modules", "dist", "tests"]
24
- }