@the-cascade-protocol/cli 0.2.0

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 (165) hide show
  1. package/.dockerignore +7 -0
  2. package/.eslintrc.json +23 -0
  3. package/.prettierrc +7 -0
  4. package/DOCKER.md +36 -0
  5. package/Dockerfile +18 -0
  6. package/README.md +69 -0
  7. package/dist/commands/capabilities.d.ts +9 -0
  8. package/dist/commands/capabilities.d.ts.map +1 -0
  9. package/dist/commands/capabilities.js +194 -0
  10. package/dist/commands/capabilities.js.map +1 -0
  11. package/dist/commands/conformance.d.ts +15 -0
  12. package/dist/commands/conformance.d.ts.map +1 -0
  13. package/dist/commands/conformance.js +348 -0
  14. package/dist/commands/conformance.js.map +1 -0
  15. package/dist/commands/convert.d.ts +21 -0
  16. package/dist/commands/convert.d.ts.map +1 -0
  17. package/dist/commands/convert.js +134 -0
  18. package/dist/commands/convert.js.map +1 -0
  19. package/dist/commands/pod/export.d.ts +8 -0
  20. package/dist/commands/pod/export.d.ts.map +1 -0
  21. package/dist/commands/pod/export.js +72 -0
  22. package/dist/commands/pod/export.js.map +1 -0
  23. package/dist/commands/pod/helpers.d.ts +79 -0
  24. package/dist/commands/pod/helpers.d.ts.map +1 -0
  25. package/dist/commands/pod/helpers.js +369 -0
  26. package/dist/commands/pod/helpers.js.map +1 -0
  27. package/dist/commands/pod/index.d.ts +20 -0
  28. package/dist/commands/pod/index.d.ts.map +1 -0
  29. package/dist/commands/pod/index.js +29 -0
  30. package/dist/commands/pod/index.js.map +1 -0
  31. package/dist/commands/pod/info.d.ts +9 -0
  32. package/dist/commands/pod/info.d.ts.map +1 -0
  33. package/dist/commands/pod/info.js +196 -0
  34. package/dist/commands/pod/info.js.map +1 -0
  35. package/dist/commands/pod/init.d.ts +9 -0
  36. package/dist/commands/pod/init.d.ts.map +1 -0
  37. package/dist/commands/pod/init.js +251 -0
  38. package/dist/commands/pod/init.js.map +1 -0
  39. package/dist/commands/pod/query.d.ts +9 -0
  40. package/dist/commands/pod/query.d.ts.map +1 -0
  41. package/dist/commands/pod/query.js +169 -0
  42. package/dist/commands/pod/query.js.map +1 -0
  43. package/dist/commands/pod 2.js +1017 -0
  44. package/dist/commands/pod.d.ts +28 -0
  45. package/dist/commands/pod.d.ts 2.map +1 -0
  46. package/dist/commands/pod.d.ts.map +1 -0
  47. package/dist/commands/pod.js +1031 -0
  48. package/dist/commands/pod.js 2.map +1 -0
  49. package/dist/commands/pod.js.map +1 -0
  50. package/dist/commands/serve.d.ts +33 -0
  51. package/dist/commands/serve.d.ts.map +1 -0
  52. package/dist/commands/serve.js +74 -0
  53. package/dist/commands/serve.js.map +1 -0
  54. package/dist/commands/validate.d.ts +18 -0
  55. package/dist/commands/validate.d.ts.map +1 -0
  56. package/dist/commands/validate.js +275 -0
  57. package/dist/commands/validate.js.map +1 -0
  58. package/dist/index.d.ts +19 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +49 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
  63. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
  64. package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
  65. package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
  66. package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
  67. package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
  68. package/dist/lib/fhir-converter/converters-clinical.js +391 -0
  69. package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
  70. package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
  71. package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
  72. package/dist/lib/fhir-converter/converters-demographics.js +242 -0
  73. package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
  74. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
  75. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
  76. package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
  77. package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
  78. package/dist/lib/fhir-converter/index.d.ts +36 -0
  79. package/dist/lib/fhir-converter/index.d.ts.map +1 -0
  80. package/dist/lib/fhir-converter/index.js +187 -0
  81. package/dist/lib/fhir-converter/index.js.map +1 -0
  82. package/dist/lib/fhir-converter/types.d.ts +77 -0
  83. package/dist/lib/fhir-converter/types.d.ts.map +1 -0
  84. package/dist/lib/fhir-converter/types.js +236 -0
  85. package/dist/lib/fhir-converter/types.js.map +1 -0
  86. package/dist/lib/fhir-converter.d.ts +62 -0
  87. package/dist/lib/fhir-converter.d.ts.map +1 -0
  88. package/dist/lib/fhir-converter.js +1474 -0
  89. package/dist/lib/fhir-converter.js.map +1 -0
  90. package/dist/lib/mcp/audit.d.ts +24 -0
  91. package/dist/lib/mcp/audit.d.ts.map +1 -0
  92. package/dist/lib/mcp/audit.js +85 -0
  93. package/dist/lib/mcp/audit.js.map +1 -0
  94. package/dist/lib/mcp/server.d.ts +38 -0
  95. package/dist/lib/mcp/server.d.ts.map +1 -0
  96. package/dist/lib/mcp/server.js +172 -0
  97. package/dist/lib/mcp/server.js.map +1 -0
  98. package/dist/lib/mcp/tools.d.ts +47 -0
  99. package/dist/lib/mcp/tools.d.ts.map +1 -0
  100. package/dist/lib/mcp/tools.js +547 -0
  101. package/dist/lib/mcp/tools.js.map +1 -0
  102. package/dist/lib/output.d.ts +26 -0
  103. package/dist/lib/output.d.ts.map +1 -0
  104. package/dist/lib/output.js +64 -0
  105. package/dist/lib/output.js.map +1 -0
  106. package/dist/lib/shacl-validator.d.ts +53 -0
  107. package/dist/lib/shacl-validator.d.ts.map +1 -0
  108. package/dist/lib/shacl-validator.js +245 -0
  109. package/dist/lib/shacl-validator.js.map +1 -0
  110. package/dist/lib/turtle-parser.d.ts +64 -0
  111. package/dist/lib/turtle-parser.d.ts.map +1 -0
  112. package/dist/lib/turtle-parser.js +236 -0
  113. package/dist/lib/turtle-parser.js.map +1 -0
  114. package/dist/shapes/checkup.shapes.ttl +1459 -0
  115. package/dist/shapes/clinical.shapes.ttl +1350 -0
  116. package/dist/shapes/clinical.ttl +1369 -0
  117. package/dist/shapes/core.shapes.ttl +450 -0
  118. package/dist/shapes/core.ttl +603 -0
  119. package/dist/shapes/coverage.shapes.ttl +214 -0
  120. package/dist/shapes/coverage.ttl +182 -0
  121. package/dist/shapes/health.shapes.ttl +697 -0
  122. package/dist/shapes/health.ttl +859 -0
  123. package/dist/shapes/pots.shapes.ttl +481 -0
  124. package/package.json +54 -0
  125. package/src/commands/capabilities.ts +235 -0
  126. package/src/commands/conformance.ts +447 -0
  127. package/src/commands/convert.ts +164 -0
  128. package/src/commands/pod/export.ts +85 -0
  129. package/src/commands/pod/helpers.ts +449 -0
  130. package/src/commands/pod/index.ts +32 -0
  131. package/src/commands/pod/info.ts +239 -0
  132. package/src/commands/pod/init.ts +273 -0
  133. package/src/commands/pod/query.ts +224 -0
  134. package/src/commands/serve.ts +92 -0
  135. package/src/commands/validate.ts +303 -0
  136. package/src/index.ts +58 -0
  137. package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
  138. package/src/lib/fhir-converter/converters-clinical.ts +446 -0
  139. package/src/lib/fhir-converter/converters-demographics.ts +270 -0
  140. package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
  141. package/src/lib/fhir-converter/index.ts +215 -0
  142. package/src/lib/fhir-converter/types.ts +318 -0
  143. package/src/lib/mcp/audit.ts +107 -0
  144. package/src/lib/mcp/server.ts +192 -0
  145. package/src/lib/mcp/tools.ts +668 -0
  146. package/src/lib/output.ts +76 -0
  147. package/src/lib/shacl-validator.ts +314 -0
  148. package/src/lib/turtle-parser.ts +277 -0
  149. package/src/shapes/checkup.shapes.ttl +1459 -0
  150. package/src/shapes/clinical.shapes.ttl +1350 -0
  151. package/src/shapes/clinical.ttl +1369 -0
  152. package/src/shapes/core.shapes.ttl +450 -0
  153. package/src/shapes/core.ttl +603 -0
  154. package/src/shapes/coverage.shapes.ttl +214 -0
  155. package/src/shapes/coverage.ttl +182 -0
  156. package/src/shapes/health.shapes.ttl +697 -0
  157. package/src/shapes/health.ttl +859 -0
  158. package/src/shapes/pots.shapes.ttl +481 -0
  159. package/test-fixtures/fhir-bundle-example.json +216 -0
  160. package/test-fixtures/fhir-medication-example.json +18 -0
  161. package/tests/cli.test.ts +126 -0
  162. package/tests/fhir-converter.test.ts +874 -0
  163. package/tests/mcp-server.test.ts +396 -0
  164. package/tests/pod.test.ts +400 -0
  165. package/tsconfig.json +24 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Output formatting utilities for consistent CLI output.
3
+ *
4
+ * Supports both human-readable text and machine-readable JSON output modes.
5
+ */
6
+
7
+ export interface OutputOptions {
8
+ json: boolean;
9
+ verbose: boolean;
10
+ }
11
+
12
+ /**
13
+ * Format data for output in either JSON or human-readable text mode.
14
+ */
15
+ export function formatOutput(data: unknown, opts: OutputOptions): string {
16
+ if (opts.json) {
17
+ return JSON.stringify(data, null, 2);
18
+ }
19
+
20
+ if (typeof data === 'string') {
21
+ return data;
22
+ }
23
+
24
+ if (Array.isArray(data)) {
25
+ return data.map((item) => formatSingleItem(item)).join('\n');
26
+ }
27
+
28
+ return formatSingleItem(data);
29
+ }
30
+
31
+ /**
32
+ * Format a single item for human-readable output.
33
+ */
34
+ function formatSingleItem(item: unknown): string {
35
+ if (typeof item === 'string') {
36
+ return item;
37
+ }
38
+
39
+ if (typeof item === 'object' && item !== null) {
40
+ const entries = Object.entries(item as Record<string, unknown>);
41
+ return entries.map(([key, value]) => ` ${key}: ${String(value)}`).join('\n');
42
+ }
43
+
44
+ return String(item);
45
+ }
46
+
47
+ /**
48
+ * Print result data to stdout.
49
+ */
50
+ export function printResult(data: unknown, opts: OutputOptions): void {
51
+ console.log(formatOutput(data, opts));
52
+ }
53
+
54
+ /**
55
+ * Print an error message to stderr.
56
+ */
57
+ export function printError(message: string, opts: OutputOptions): void {
58
+ if (opts.json) {
59
+ console.error(JSON.stringify({ error: message }));
60
+ } else {
61
+ console.error(`ERROR: ${message}`);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Print a verbose/debug message (only if verbose mode is enabled).
67
+ */
68
+ export function printVerbose(message: string, opts: OutputOptions): void {
69
+ if (opts.verbose) {
70
+ if (opts.json) {
71
+ console.error(JSON.stringify({ debug: message }));
72
+ } else {
73
+ console.error(`[verbose] ${message}`);
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * SHACL validation utilities.
3
+ *
4
+ * Wraps rdf-validate-shacl to validate RDF data against
5
+ * Cascade Protocol SHACL shapes.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { Parser, Store } from 'n3';
12
+ import SHACLValidator from 'rdf-validate-shacl';
13
+ import { parseTurtle, detectVocabularies, CASCADE_NAMESPACES } from './turtle-parser.js';
14
+ import type { ParseResult } from './turtle-parser.js';
15
+
16
+ export interface ValidationResult {
17
+ valid: boolean;
18
+ file: string;
19
+ results: ValidationIssue[];
20
+ shapesUsed: string[];
21
+ quadCount: number;
22
+ subjects: Array<{ uri: string; types: string[] }>;
23
+ }
24
+
25
+ export interface ValidationIssue {
26
+ severity: 'violation' | 'warning' | 'info';
27
+ shape: string;
28
+ property: string;
29
+ message: string;
30
+ focusNode?: string;
31
+ value?: string;
32
+ specLink?: string;
33
+ }
34
+
35
+ /** Mapping from shape file prefixes to their documentation base URLs */
36
+ const SPEC_BASE_URLS: Record<string, string> = {
37
+ core: 'https://cascadeprotocol.org/docs/core/v1',
38
+ health: 'https://cascadeprotocol.org/docs/health/v1',
39
+ clinical: 'https://cascadeprotocol.org/docs/clinical/v1',
40
+ pots: 'https://cascadeprotocol.org/docs/pots/v1',
41
+ checkup: 'https://cascadeprotocol.org/docs/checkup/v1',
42
+ coverage: 'https://cascadeprotocol.org/docs/coverage/v1',
43
+ };
44
+
45
+ /**
46
+ * Resolve the bundled shapes directory.
47
+ * Works from both src/ (dev via tsx) and dist/ (built).
48
+ */
49
+ function getShapesDir(): string {
50
+ const __filename = fileURLToPath(import.meta.url);
51
+ const __dirname = path.dirname(__filename);
52
+
53
+ // When running from dist/lib/, shapes are at dist/shapes/
54
+ // When running from src/lib/ (dev), shapes are at src/shapes/
55
+ const shapesDir = path.resolve(__dirname, '..', 'shapes');
56
+
57
+ if (!fs.existsSync(shapesDir)) {
58
+ throw new Error(
59
+ `Shapes directory not found at ${shapesDir}. ` +
60
+ 'Run "npm run build" to bundle shapes.',
61
+ );
62
+ }
63
+ return shapesDir;
64
+ }
65
+
66
+ /**
67
+ * Load and parse a Turtle file into an N3 Store.
68
+ */
69
+ function loadTurtleFile(filePath: string): Store {
70
+ const content = fs.readFileSync(filePath, 'utf-8');
71
+ const parser = new Parser({ baseIRI: '' });
72
+ const store = new Store();
73
+ const quads = parser.parse(content);
74
+ store.addQuads(quads);
75
+ return store;
76
+ }
77
+
78
+ /**
79
+ * Load all bundled SHACL shapes from the shapes directory into a single store.
80
+ * If a custom shapes path is provided, load from there instead.
81
+ */
82
+ export function loadShapes(customShapesPath?: string): { store: Store; shapeFiles: string[] } {
83
+ const shapesDir = customShapesPath ?? getShapesDir();
84
+ const store = new Store();
85
+ const shapeFiles: string[] = [];
86
+
87
+ if (!fs.existsSync(shapesDir)) {
88
+ throw new Error(`Shapes directory not found: ${shapesDir}`);
89
+ }
90
+
91
+ const files = fs.readdirSync(shapesDir).filter((f) => f.endsWith('.shapes.ttl'));
92
+
93
+ if (files.length === 0) {
94
+ throw new Error(`No SHACL shape files (*.shapes.ttl) found in ${shapesDir}`);
95
+ }
96
+
97
+ for (const file of files) {
98
+ const filePath = path.join(shapesDir, file);
99
+ const fileStore = loadTurtleFile(filePath);
100
+ for (const quad of fileStore) {
101
+ store.addQuad(quad);
102
+ }
103
+ shapeFiles.push(file);
104
+ }
105
+
106
+ // Also load vocabulary/ontology files so the validator knows about class hierarchies
107
+ const vocabFiles = fs.readdirSync(shapesDir).filter(
108
+ (f) => f.endsWith('.ttl') && !f.endsWith('.shapes.ttl'),
109
+ );
110
+
111
+ for (const file of vocabFiles) {
112
+ const filePath = path.join(shapesDir, file);
113
+ const fileStore = loadTurtleFile(filePath);
114
+ for (const quad of fileStore) {
115
+ store.addQuad(quad);
116
+ }
117
+ }
118
+
119
+ return { store, shapeFiles };
120
+ }
121
+
122
+ /**
123
+ * Generate a spec link from a shape URI.
124
+ *
125
+ * Examples:
126
+ * https://ns.cascadeprotocol.org/clinical/v1#MedicationShape
127
+ * -> https://cascadeprotocol.org/docs/clinical/v1#Medication
128
+ * https://ns.cascadeprotocol.org/health/v1#SelfReportShape
129
+ * -> https://cascadeprotocol.org/docs/health/v1#SelfReport
130
+ */
131
+ function generateSpecLink(shapeUri: string): string | undefined {
132
+ for (const [vocab, ns] of Object.entries(CASCADE_NAMESPACES)) {
133
+ if (shapeUri.startsWith(ns)) {
134
+ const localName = shapeUri.slice(ns.length);
135
+ // Remove "Shape" suffix for the spec link
136
+ const className = localName.replace(/Shape$/, '');
137
+ const baseUrl = SPEC_BASE_URLS[vocab];
138
+ if (baseUrl) {
139
+ return `${baseUrl}#${className}`;
140
+ }
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+
146
+ /**
147
+ * Map SHACL severity URI to our severity level.
148
+ */
149
+ function mapSeverity(severityUri: string): 'violation' | 'warning' | 'info' {
150
+ if (severityUri.endsWith('#Violation') || severityUri.endsWith('Violation')) {
151
+ return 'violation';
152
+ }
153
+ if (severityUri.endsWith('#Warning') || severityUri.endsWith('Warning')) {
154
+ return 'warning';
155
+ }
156
+ if (severityUri.endsWith('#Info') || severityUri.endsWith('Info')) {
157
+ return 'info';
158
+ }
159
+ // Default to violation for unknown severity
160
+ return 'violation';
161
+ }
162
+
163
+ /**
164
+ * Extract a human-readable name from a URI by taking the fragment or last path segment.
165
+ */
166
+ function uriToName(uri: string): string {
167
+ if (uri.includes('#')) {
168
+ return uri.split('#').pop() ?? uri;
169
+ }
170
+ return uri.split('/').pop() ?? uri;
171
+ }
172
+
173
+ /**
174
+ * Validate an already-parsed Turtle file against SHACL shapes.
175
+ */
176
+ export function validateParsed(
177
+ parseResult: ParseResult,
178
+ shapesStore: Store,
179
+ shapeFiles: string[],
180
+ filePath: string,
181
+ ): ValidationResult {
182
+ if (!parseResult.success) {
183
+ return {
184
+ valid: false,
185
+ file: filePath,
186
+ results: parseResult.errors.map((err) => ({
187
+ severity: 'violation' as const,
188
+ shape: '',
189
+ property: '',
190
+ message: `Parse error: ${err}`,
191
+ })),
192
+ shapesUsed: [],
193
+ quadCount: 0,
194
+ subjects: [],
195
+ };
196
+ }
197
+
198
+ // Detect vocabularies used so we can report which shapes apply
199
+ const vocabsUsed = detectVocabularies(parseResult);
200
+ const shapesUsed = shapeFiles.filter((f) => {
201
+ const vocabName = f.replace('.shapes.ttl', '');
202
+ return vocabsUsed.includes(vocabName);
203
+ });
204
+
205
+ // Run SHACL validation
206
+ const validator = new SHACLValidator(shapesStore, { allowNamedNodeInList: true });
207
+ const report = validator.validate(parseResult.store);
208
+
209
+ // Map results to our interface
210
+ const issues: ValidationIssue[] = [];
211
+
212
+ for (const result of report.results) {
213
+ const severityUri = result.severity?.value ?? '';
214
+ const severity = mapSeverity(severityUri);
215
+
216
+ const shapeUri = result.sourceShape?.value ?? '';
217
+ const pathUri = result.path?.value ?? '';
218
+ const focusNodeUri = result.focusNode?.value ?? '';
219
+ const valueStr = result.value?.value;
220
+
221
+ // Get the message - result.message is an array of Terms
222
+ const messages = result.message ?? [];
223
+ const messageText = messages.length > 0
224
+ ? messages.map((m) => m.value).join('; ')
225
+ : `Constraint violation on ${uriToName(pathUri)} of ${uriToName(shapeUri)}`;
226
+
227
+ const specLink = generateSpecLink(shapeUri);
228
+
229
+ issues.push({
230
+ severity,
231
+ shape: uriToName(shapeUri),
232
+ property: uriToName(pathUri),
233
+ message: messageText,
234
+ focusNode: focusNodeUri || undefined,
235
+ value: valueStr,
236
+ specLink,
237
+ });
238
+ }
239
+
240
+ return {
241
+ valid: report.conforms,
242
+ file: filePath,
243
+ results: issues,
244
+ shapesUsed,
245
+ quadCount: parseResult.quadCount,
246
+ subjects: parseResult.subjects,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Validate a Turtle string against SHACL shapes.
252
+ */
253
+ export function validateTurtle(
254
+ turtleContent: string,
255
+ shapesStore: Store,
256
+ shapeFiles: string[],
257
+ filePath: string,
258
+ ): ValidationResult {
259
+ const parseResult = parseTurtle(turtleContent);
260
+ return validateParsed(parseResult, shapesStore, shapeFiles, filePath);
261
+ }
262
+
263
+ /**
264
+ * Validate a Turtle file against SHACL shapes.
265
+ */
266
+ export function validateFile(
267
+ filePath: string,
268
+ shapesStore: Store,
269
+ shapeFiles: string[],
270
+ ): ValidationResult {
271
+ if (!fs.existsSync(filePath)) {
272
+ return {
273
+ valid: false,
274
+ file: filePath,
275
+ results: [{
276
+ severity: 'violation',
277
+ shape: '',
278
+ property: '',
279
+ message: `File not found: ${filePath}`,
280
+ }],
281
+ shapesUsed: [],
282
+ quadCount: 0,
283
+ subjects: [],
284
+ };
285
+ }
286
+
287
+ const content = fs.readFileSync(filePath, 'utf-8');
288
+ return validateTurtle(content, shapesStore, shapeFiles, filePath);
289
+ }
290
+
291
+ /**
292
+ * Recursively find all .ttl files in a directory.
293
+ */
294
+ export function findTurtleFiles(dirPath: string): string[] {
295
+ const results: string[] = [];
296
+
297
+ function walk(dir: string): void {
298
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
299
+ for (const entry of entries) {
300
+ const fullPath = path.join(dir, entry.name);
301
+ if (entry.isDirectory()) {
302
+ // Skip hidden directories and node_modules
303
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
304
+ walk(fullPath);
305
+ }
306
+ } else if (entry.isFile() && entry.name.endsWith('.ttl')) {
307
+ results.push(fullPath);
308
+ }
309
+ }
310
+ }
311
+
312
+ walk(dirPath);
313
+ return results.sort();
314
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Turtle parsing utilities.
3
+ *
4
+ * Wraps the n3 library to provide Cascade-specific Turtle parsing
5
+ * with support for Cascade Protocol namespaces.
6
+ */
7
+
8
+ import { Parser, Store, DataFactory } from 'n3';
9
+ import type { Quad } from 'n3';
10
+ import * as fs from 'fs/promises';
11
+
12
+ const { namedNode } = DataFactory;
13
+
14
+ /** Well-known RDF predicates */
15
+ const RDF_TYPE_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
16
+
17
+ /** Known Cascade Protocol namespace prefixes */
18
+ export const CASCADE_NAMESPACES: Record<string, string> = {
19
+ core: 'https://ns.cascadeprotocol.org/core/v1#',
20
+ cascade: 'https://ns.cascadeprotocol.org/core/v1#',
21
+ health: 'https://ns.cascadeprotocol.org/health/v1#',
22
+ clinical: 'https://ns.cascadeprotocol.org/clinical/v1#',
23
+ pots: 'https://ns.cascadeprotocol.org/pots/v1#',
24
+ checkup: 'https://ns.cascadeprotocol.org/checkup/v1#',
25
+ coverage: 'https://ns.cascadeprotocol.org/coverage/v1#',
26
+ } as const;
27
+
28
+ /** Well-known non-Cascade namespaces for IRI shortening */
29
+ const WELL_KNOWN_NAMESPACES: Record<string, string> = {
30
+ foaf: 'http://xmlns.com/foaf/0.1/',
31
+ solid: 'http://www.w3.org/ns/solid/terms#',
32
+ ldp: 'http://www.w3.org/ns/ldp#',
33
+ dct: 'http://purl.org/dc/terms/',
34
+ dcterms: 'http://purl.org/dc/terms/',
35
+ prov: 'http://www.w3.org/ns/prov#',
36
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
37
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
38
+ fhir: 'http://hl7.org/fhir/',
39
+ rxnorm: 'http://www.nlm.nih.gov/research/umls/rxnorm/',
40
+ sct: 'http://snomed.info/sct/',
41
+ loinc: 'http://loinc.org/rdf#',
42
+ icd10: 'http://hl7.org/fhir/sid/icd-10-cm/',
43
+ cvx: 'http://hl7.org/fhir/sid/cvx/',
44
+ };
45
+
46
+ export interface ParseResult {
47
+ success: boolean;
48
+ quads: Quad[];
49
+ store: Store;
50
+ quadCount: number;
51
+ prefixes: Record<string, string>;
52
+ errors: string[];
53
+ subjects: SubjectInfo[];
54
+ }
55
+
56
+ export interface SubjectInfo {
57
+ uri: string;
58
+ types: string[];
59
+ }
60
+
61
+ /**
62
+ * Parse a Turtle string into quads and a Store.
63
+ */
64
+ export function parseTurtle(input: string, baseIRI?: string): ParseResult {
65
+ const parser = new Parser({ baseIRI: baseIRI ?? '' });
66
+ const store = new Store();
67
+ const prefixes: Record<string, string> = {};
68
+ const errors: string[] = [];
69
+
70
+ let quads: Quad[];
71
+ try {
72
+ quads = parser.parse(input, null, (prefix, prefixNode) => {
73
+ prefixes[prefix] = prefixNode.value;
74
+ });
75
+ store.addQuads(quads);
76
+ } catch (e: unknown) {
77
+ const msg = e instanceof Error ? e.message : String(e);
78
+ return {
79
+ success: false,
80
+ quads: [],
81
+ store,
82
+ quadCount: 0,
83
+ prefixes,
84
+ errors: [msg],
85
+ subjects: [],
86
+ };
87
+ }
88
+
89
+ // Extract subjects with their types
90
+ const subjects = extractSubjectsWithTypes(store);
91
+
92
+ return {
93
+ success: true,
94
+ quads,
95
+ store,
96
+ quadCount: quads.length,
97
+ prefixes,
98
+ errors,
99
+ subjects,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Extract all subjects with their rdf:type values from a store.
105
+ */
106
+ function extractSubjectsWithTypes(store: Store): SubjectInfo[] {
107
+ const subjectMap = new Map<string, string[]>();
108
+
109
+ const typeQuads = store.getQuads(null, namedNode(RDF_TYPE_IRI), null, null);
110
+ for (const quad of typeQuads) {
111
+ const subjectUri = quad.subject.value;
112
+ const typeUri = quad.object.value;
113
+ if (!subjectMap.has(subjectUri)) {
114
+ subjectMap.set(subjectUri, []);
115
+ }
116
+ subjectMap.get(subjectUri)!.push(typeUri);
117
+ }
118
+
119
+ return Array.from(subjectMap.entries()).map(([uri, types]) => ({ uri, types }));
120
+ }
121
+
122
+ /**
123
+ * Detect which Cascade Protocol vocabularies are used in a parsed Turtle file
124
+ * based on prefixes and type URIs.
125
+ */
126
+ export function detectVocabularies(result: ParseResult): string[] {
127
+ const vocabs = new Set<string>();
128
+
129
+ // Check prefix declarations
130
+ for (const [, iri] of Object.entries(result.prefixes)) {
131
+ for (const [name, ns] of Object.entries(CASCADE_NAMESPACES)) {
132
+ if (iri === ns || iri.startsWith(ns.replace(/#$/, ''))) {
133
+ vocabs.add(name);
134
+ }
135
+ }
136
+ }
137
+
138
+ // Check type URIs
139
+ for (const subject of result.subjects) {
140
+ for (const typeUri of subject.types) {
141
+ for (const [name, ns] of Object.entries(CASCADE_NAMESPACES)) {
142
+ if (typeUri.startsWith(ns)) {
143
+ vocabs.add(name);
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ // Also scan all quads for namespace usage (predicates, objects)
150
+ for (const quad of result.quads) {
151
+ const values = [quad.predicate.value, quad.object.value];
152
+ for (const val of values) {
153
+ for (const [name, ns] of Object.entries(CASCADE_NAMESPACES)) {
154
+ if (val.startsWith(ns)) {
155
+ vocabs.add(name);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ return Array.from(vocabs);
162
+ }
163
+
164
+ /**
165
+ * Parse a Turtle file from disk.
166
+ */
167
+ export async function parseTurtleFile(filePath: string): Promise<ParseResult> {
168
+ try {
169
+ const content = await fs.readFile(filePath, 'utf-8');
170
+ return parseTurtle(content, `file://${filePath}`);
171
+ } catch (err: unknown) {
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ return {
174
+ success: false,
175
+ quads: [],
176
+ store: new Store(),
177
+ quadCount: 0,
178
+ prefixes: {},
179
+ errors: [`Failed to read file ${filePath}: ${message}`],
180
+ subjects: [],
181
+ };
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Return all subject URIs that have rdf:type matching the given type IRI.
187
+ */
188
+ export function getSubjectsByType(store: Store, rdfType: string): string[] {
189
+ const subjects = store.getSubjects(namedNode(RDF_TYPE_IRI), namedNode(rdfType), null);
190
+ return subjects.map((s) => s.value);
191
+ }
192
+
193
+ /**
194
+ * Return all predicate-value pairs for a given subject.
195
+ * Returns a map of predicate IRI -> array of object values.
196
+ */
197
+ export function getProperties(store: Store, subject: string): Record<string, string[]> {
198
+ const result: Record<string, string[]> = {};
199
+ const quads = store.getQuads(subject as never, null, null, null);
200
+
201
+ for (const quad of quads) {
202
+ const predicate = quad.predicate.value;
203
+ if (!result[predicate]) {
204
+ result[predicate] = [];
205
+ }
206
+ result[predicate].push(quad.object.value);
207
+ }
208
+
209
+ return result;
210
+ }
211
+
212
+ /**
213
+ * Count subjects of a given rdf:type.
214
+ */
215
+ export function countSubjectsByType(store: Store, rdfType: string): number {
216
+ return store.countQuads(null, namedNode(RDF_TYPE_IRI), namedNode(rdfType), null);
217
+ }
218
+
219
+ /**
220
+ * Get all unique rdf:type values in the store.
221
+ */
222
+ export function getAllTypes(store: Store): string[] {
223
+ const objects = store.getObjects(null, namedNode(RDF_TYPE_IRI), null);
224
+ const types = new Set<string>();
225
+ for (const obj of objects) {
226
+ types.add(obj.value);
227
+ }
228
+ return Array.from(types);
229
+ }
230
+
231
+ /**
232
+ * Shorten a full IRI using known Cascade namespace prefixes.
233
+ * Returns the prefixed form (e.g., "health:MedicationRecord") or the full IRI if no match.
234
+ */
235
+ export function shortenIRI(iri: string, prefixes?: Record<string, string>): string {
236
+ const allPrefixes: Record<string, string> = {
237
+ ...CASCADE_NAMESPACES,
238
+ ...WELL_KNOWN_NAMESPACES,
239
+ ...(prefixes ?? {}),
240
+ };
241
+
242
+ for (const [prefix, namespace] of Object.entries(allPrefixes)) {
243
+ if (iri.startsWith(namespace)) {
244
+ return `${prefix}:${iri.slice(namespace.length)}`;
245
+ }
246
+ }
247
+ return iri;
248
+ }
249
+
250
+ /**
251
+ * Extract a human-readable label for a record from its properties.
252
+ * Tries common name predicates in order of preference.
253
+ */
254
+ export function extractLabel(props: Record<string, string[]>): string | undefined {
255
+ const namePredicates = [
256
+ CASCADE_NAMESPACES.health + 'medicationName',
257
+ CASCADE_NAMESPACES.health + 'conditionName',
258
+ CASCADE_NAMESPACES.health + 'allergen',
259
+ CASCADE_NAMESPACES.clinical + 'supplementName',
260
+ CASCADE_NAMESPACES.clinical + 'vaccineName',
261
+ CASCADE_NAMESPACES.health + 'vaccineName',
262
+ CASCADE_NAMESPACES.health + 'testName',
263
+ CASCADE_NAMESPACES.health + 'labTestName',
264
+ CASCADE_NAMESPACES.clinical + 'planName',
265
+ CASCADE_NAMESPACES.cascade + 'planName',
266
+ 'http://xmlns.com/foaf/0.1/name',
267
+ 'http://xmlns.com/foaf/0.1/givenName',
268
+ 'http://purl.org/dc/terms/title',
269
+ ];
270
+
271
+ for (const pred of namePredicates) {
272
+ if (props[pred]?.[0]) {
273
+ return props[pred][0];
274
+ }
275
+ }
276
+ return undefined;
277
+ }