@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,235 @@
1
+ /**
2
+ * cascade capabilities
3
+ *
4
+ * Output a machine-readable JSON description of all CLI commands and their
5
+ * parameters. Designed for consumption by AI agents and tooling.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { printResult, type OutputOptions } from '../lib/output.js';
10
+
11
+ /** Tool parameter descriptor for capabilities output */
12
+ interface ToolParameter {
13
+ name: string;
14
+ type: string;
15
+ required: boolean;
16
+ description: string;
17
+ default?: string;
18
+ choices?: string[];
19
+ }
20
+
21
+ /** Tool descriptor for capabilities output */
22
+ interface ToolDescriptor {
23
+ name: string;
24
+ description: string;
25
+ usage: string;
26
+ parameters: ToolParameter[];
27
+ examples: string[];
28
+ status: 'implemented';
29
+ }
30
+
31
+ /** MCP tool descriptor */
32
+ interface McpToolDescriptor {
33
+ name: string;
34
+ description: string;
35
+ parameters: Record<string, { type: string; description: string; required: boolean; enum?: string[] }>;
36
+ }
37
+
38
+ /** Full capabilities output */
39
+ interface CapabilitiesOutput {
40
+ name: string;
41
+ version: string;
42
+ description: string;
43
+ protocol: string;
44
+ tools: ToolDescriptor[];
45
+ mcpTools: McpToolDescriptor[];
46
+ securityModel: Record<string, string>;
47
+ }
48
+
49
+ function getCapabilities(version: string): CapabilitiesOutput {
50
+ return {
51
+ name: '@the-cascade-protocol/cli',
52
+ version,
53
+ description: 'Cascade Protocol CLI - Validate, convert, and manage health data with zero network calls',
54
+ protocol: 'https://cascadeprotocol.org',
55
+ securityModel: {
56
+ networkCalls: 'zero — all operations are local',
57
+ dataStorage: 'local filesystem only',
58
+ provenance: 'all agent-written data tagged with AIGenerated provenance',
59
+ auditLog: 'all MCP operations logged to provenance/audit-log.ttl',
60
+ },
61
+ tools: [
62
+ {
63
+ name: 'validate',
64
+ description: 'Validate Cascade Protocol data against SHACL shapes',
65
+ usage: 'cascade validate <file-or-dir> [options]',
66
+ parameters: [
67
+ { name: 'file-or-dir', type: 'string', required: true, description: 'Turtle file or directory to validate' },
68
+ { name: '--shapes', type: 'string', required: false, description: 'Path to custom SHACL shapes directory' },
69
+ { name: '--json', type: 'boolean', required: false, description: 'Output results as JSON' },
70
+ { name: '--verbose', type: 'boolean', required: false, description: 'Show detailed validation information' },
71
+ ],
72
+ examples: ['cascade validate record.ttl', 'cascade validate ./data/ --json'],
73
+ status: 'implemented',
74
+ },
75
+ {
76
+ name: 'convert',
77
+ description: 'Convert between health data formats (FHIR R4 JSON, Cascade Turtle, JSON-LD)',
78
+ usage: 'cascade convert [file] --from <format> --to <format> [options]',
79
+ parameters: [
80
+ { name: 'file', type: 'string', required: false, description: 'Input file (reads from stdin if omitted)' },
81
+ { name: '--from', type: 'string', required: true, description: 'Source format', choices: ['fhir', 'cascade'] },
82
+ { name: '--to', type: 'string', required: true, description: 'Target format', choices: ['turtle', 'jsonld', 'fhir'] },
83
+ { name: '--format', type: 'string', required: false, description: 'Output serialization format', default: 'turtle', choices: ['turtle', 'jsonld'] },
84
+ { name: '--json', type: 'boolean', required: false, description: 'Output results as JSON' },
85
+ ],
86
+ examples: ['cascade convert patient.json --from fhir --to turtle', 'cat data.json | cascade convert --from fhir --to turtle'],
87
+ status: 'implemented',
88
+ },
89
+ {
90
+ name: 'pod init',
91
+ description: 'Initialize a new Cascade Pod directory structure',
92
+ usage: 'cascade pod init <directory>',
93
+ parameters: [{ name: 'directory', type: 'string', required: true, description: 'Directory to initialize as a Cascade Pod' }],
94
+ examples: ['cascade pod init ./my-pod'],
95
+ status: 'implemented',
96
+ },
97
+ {
98
+ name: 'pod query',
99
+ description: 'Query data within a Cascade Pod by type',
100
+ usage: 'cascade pod query <pod-dir> [options]',
101
+ parameters: [
102
+ { name: 'pod-dir', type: 'string', required: true, description: 'Path to the Cascade Pod' },
103
+ { name: '--medications', type: 'boolean', required: false, description: 'Query medications' },
104
+ { name: '--conditions', type: 'boolean', required: false, description: 'Query conditions' },
105
+ { name: '--allergies', type: 'boolean', required: false, description: 'Query allergies' },
106
+ { name: '--lab-results', type: 'boolean', required: false, description: 'Query lab results' },
107
+ { name: '--all', type: 'boolean', required: false, description: 'Query all data' },
108
+ { name: '--json', type: 'boolean', required: false, description: 'Output as JSON' },
109
+ ],
110
+ examples: ['cascade pod query ./my-pod --medications --json', 'cascade pod query ./my-pod --all --json'],
111
+ status: 'implemented',
112
+ },
113
+ {
114
+ name: 'pod info',
115
+ description: 'Show Cascade Pod metadata and statistics',
116
+ usage: 'cascade pod info <pod-dir>',
117
+ parameters: [{ name: 'pod-dir', type: 'string', required: true, description: 'Path to the Cascade Pod' }],
118
+ examples: ['cascade pod info ./my-pod'],
119
+ status: 'implemented',
120
+ },
121
+ {
122
+ name: 'pod export',
123
+ description: 'Export Cascade Pod data as ZIP or directory',
124
+ usage: 'cascade pod export <pod-dir> [options]',
125
+ parameters: [
126
+ { name: 'pod-dir', type: 'string', required: true, description: 'Path to the Cascade Pod' },
127
+ { name: '--format', type: 'string', required: false, description: 'Export format', default: 'zip', choices: ['zip', 'directory'] },
128
+ ],
129
+ examples: ['cascade pod export ./my-pod', 'cascade pod export ./my-pod --format directory'],
130
+ status: 'implemented',
131
+ },
132
+ {
133
+ name: 'conformance run',
134
+ description: 'Run conformance test suite (53 fixtures)',
135
+ usage: 'cascade conformance run --suite <fixtures-dir> --self',
136
+ parameters: [
137
+ { name: '--suite', type: 'string', required: true, description: 'Path to test fixtures directory' },
138
+ { name: '--self', type: 'boolean', required: false, description: 'Run self-conformance tests' },
139
+ { name: '--json', type: 'boolean', required: false, description: 'Output results as JSON' },
140
+ ],
141
+ examples: ['cascade conformance run --suite ./fixtures --self'],
142
+ status: 'implemented',
143
+ },
144
+ {
145
+ name: 'serve',
146
+ description: 'Start local MCP-compatible agent server',
147
+ usage: 'cascade serve --mcp [options]',
148
+ parameters: [
149
+ { name: '--mcp', type: 'boolean', required: true, description: 'Enable MCP (Model Context Protocol) mode' },
150
+ { name: '--transport', type: 'string', required: false, description: 'Transport type', default: 'stdio', choices: ['stdio', 'sse'] },
151
+ { name: '--port', type: 'string', required: false, description: 'Port for SSE transport', default: '3000' },
152
+ { name: '--pod', type: 'string', required: false, description: 'Default Pod directory path' },
153
+ ],
154
+ examples: ['cascade serve --mcp', 'cascade serve --mcp --transport sse --port 3000'],
155
+ status: 'implemented',
156
+ },
157
+ {
158
+ name: 'capabilities',
159
+ description: 'Show machine-readable JSON description of all CLI tools and MCP tools',
160
+ usage: 'cascade capabilities',
161
+ parameters: [],
162
+ examples: ['cascade capabilities'],
163
+ status: 'implemented',
164
+ },
165
+ ],
166
+ mcpTools: [
167
+ {
168
+ name: 'cascade_pod_read',
169
+ description: 'Read a Cascade Pod and return a JSON summary of all contents',
170
+ parameters: { path: { type: 'string', description: 'Pod directory path (optional, uses CASCADE_POD_PATH)', required: false } },
171
+ },
172
+ {
173
+ name: 'cascade_pod_query',
174
+ description: 'Query records from a Pod by data type',
175
+ parameters: {
176
+ path: { type: 'string', description: 'Pod directory path (optional)', required: false },
177
+ dataType: { type: 'string', description: 'Data type to query', required: true, enum: ['medications', 'conditions', 'allergies', 'lab-results', 'immunizations', 'vital-signs', 'supplements', 'insurance', 'patient-profile', 'heart-rate', 'blood-pressure', 'activity', 'sleep', 'all'] },
178
+ },
179
+ },
180
+ {
181
+ name: 'cascade_validate',
182
+ description: 'Validate Turtle data against SHACL shapes',
183
+ parameters: {
184
+ path: { type: 'string', description: 'File or directory path', required: false },
185
+ content: { type: 'string', description: 'Inline Turtle content', required: false },
186
+ },
187
+ },
188
+ {
189
+ name: 'cascade_convert',
190
+ description: 'Convert between FHIR R4 JSON and Cascade Turtle/JSON-LD',
191
+ parameters: {
192
+ content: { type: 'string', description: 'Content to convert', required: true },
193
+ from: { type: 'string', description: 'Source format', required: true, enum: ['fhir', 'cascade'] },
194
+ to: { type: 'string', description: 'Target format', required: true, enum: ['cascade', 'fhir'] },
195
+ format: { type: 'string', description: 'Output format (turtle or jsonld)', required: false, enum: ['turtle', 'jsonld'] },
196
+ },
197
+ },
198
+ {
199
+ name: 'cascade_write',
200
+ description: 'Write a health record to a Pod with AIGenerated provenance',
201
+ parameters: {
202
+ path: { type: 'string', description: 'Pod directory path (optional)', required: false },
203
+ dataType: { type: 'string', description: 'Record type', required: true, enum: ['medications', 'conditions', 'allergies', 'lab-results', 'immunizations', 'vital-signs', 'supplements'] },
204
+ record: { type: 'object', description: 'JSON object with record fields', required: true },
205
+ provenance: { type: 'object', description: 'Provenance metadata (agentId, reason, confidence, sourceRecords)', required: false },
206
+ },
207
+ },
208
+ {
209
+ name: 'cascade_capabilities',
210
+ description: 'Describe all available tools and their schemas',
211
+ parameters: {},
212
+ },
213
+ ],
214
+ };
215
+ }
216
+
217
+ export function registerCapabilitiesCommand(program: Command): void {
218
+ program
219
+ .command('capabilities')
220
+ .description('Show machine-readable tool descriptions')
221
+ .action(() => {
222
+ const globalOpts = program.opts() as OutputOptions;
223
+ const version = program.version() ?? '0.1.0';
224
+ const capabilities = getCapabilities(version);
225
+
226
+ // Capabilities always outputs JSON, regardless of --json flag
227
+ // This is intentional: the purpose of this command is machine-readable output
228
+ if (globalOpts.json) {
229
+ printResult(capabilities, { json: true, verbose: globalOpts.verbose });
230
+ } else {
231
+ // Even in non-JSON mode, output formatted JSON for readability
232
+ printResult(capabilities, { json: true, verbose: globalOpts.verbose });
233
+ }
234
+ });
235
+ }
@@ -0,0 +1,447 @@
1
+ /**
2
+ * cascade conformance run
3
+ *
4
+ * Run conformance test suite against a CLI command or self-test.
5
+ *
6
+ * Options:
7
+ * --suite <fixtures-dir> Path to test fixtures directory
8
+ * --command "<cmd>" External command to test
9
+ * --self Run self-conformance tests
10
+ * --json Output results as JSON
11
+ * --verbose Show detailed test output
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { Command } from 'commander';
17
+ import { loadShapes, validateTurtle } from '../lib/shacl-validator.js';
18
+ import { parseTurtle } from '../lib/turtle-parser.js';
19
+ import { printResult, printError, printVerbose, type OutputOptions } from '../lib/output.js';
20
+ import type { ValidationResult } from '../lib/shacl-validator.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Shape of a single conformance fixture JSON file. */
27
+ interface ConformanceFixture {
28
+ id: string;
29
+ description: string;
30
+ dataType: string;
31
+ vocabulary: string;
32
+ input: Record<string, unknown>;
33
+ expectedOutput: {
34
+ turtle: string;
35
+ validationMode: 'shacl-valid' | 'exact-match';
36
+ };
37
+ shouldAccept: boolean;
38
+ tags: string[];
39
+ shaclConstraintViolated?: string;
40
+ notes?: string;
41
+ }
42
+
43
+ /** Result of running a single fixture. */
44
+ interface FixtureResult {
45
+ id: string;
46
+ description: string;
47
+ dataType: string;
48
+ status: 'passed' | 'failed' | 'error';
49
+ negative: boolean;
50
+ details?: string;
51
+ validationDetails?: ValidationResult;
52
+ }
53
+
54
+ /** Aggregate report for the full suite run. */
55
+ interface SuiteReport {
56
+ suite: string;
57
+ mode: string;
58
+ total: number;
59
+ passed: number;
60
+ failed: number;
61
+ errors: number;
62
+ results: FixtureResult[];
63
+ byDataType: Record<string, { total: number; passed: number; failed: number; errors: number }>;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Fixture loading
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Load and parse all `.json` fixture files from the given directory.
72
+ * Files are sorted by id so output order is deterministic.
73
+ */
74
+ function loadFixtures(suiteDir: string): ConformanceFixture[] {
75
+ if (!fs.existsSync(suiteDir)) {
76
+ throw new Error(`Fixtures directory not found: ${suiteDir}`);
77
+ }
78
+
79
+ const files = fs
80
+ .readdirSync(suiteDir)
81
+ .filter((f) => f.endsWith('.json'))
82
+ .sort();
83
+
84
+ if (files.length === 0) {
85
+ throw new Error(`No .json fixture files found in ${suiteDir}`);
86
+ }
87
+
88
+ const fixtures: ConformanceFixture[] = [];
89
+
90
+ for (const file of files) {
91
+ const filePath = path.join(suiteDir, file);
92
+ const raw = fs.readFileSync(filePath, 'utf-8');
93
+ const fixture = JSON.parse(raw) as ConformanceFixture;
94
+ fixtures.push(fixture);
95
+ }
96
+
97
+ return fixtures;
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Self-conformance runner
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /**
105
+ * Run a single fixture in self-conformance mode.
106
+ *
107
+ * Positive fixtures (shouldAccept === true):
108
+ * - Parse the expected turtle
109
+ * - Run SHACL validation
110
+ * - PASS if no violations; FAIL otherwise
111
+ *
112
+ * Negative fixtures (shouldAccept === false):
113
+ * - If turtle is empty: PASS (rejection before serialization)
114
+ * - If turtle is non-empty: SHACL validate — PASS if violations found, FAIL if clean
115
+ */
116
+ function runSelfFixture(
117
+ fixture: ConformanceFixture,
118
+ shapesStore: ReturnType<typeof loadShapes>['store'],
119
+ shapeFiles: string[],
120
+ opts: OutputOptions,
121
+ ): FixtureResult {
122
+ const base: Pick<FixtureResult, 'id' | 'description' | 'dataType' | 'negative'> = {
123
+ id: fixture.id,
124
+ description: fixture.description,
125
+ dataType: fixture.dataType,
126
+ negative: !fixture.shouldAccept,
127
+ };
128
+
129
+ const turtle = fixture.expectedOutput.turtle;
130
+ const validationMode = fixture.expectedOutput.validationMode;
131
+
132
+ try {
133
+ if (fixture.shouldAccept) {
134
+ // ---- Positive fixture ----
135
+
136
+ // Step 1: Parse the turtle
137
+ const parseResult = parseTurtle(turtle);
138
+ if (!parseResult.success) {
139
+ return {
140
+ ...base,
141
+ status: 'failed',
142
+ details: `Turtle parse error: ${parseResult.errors.join('; ')}`,
143
+ };
144
+ }
145
+
146
+ // Step 2: SHACL validation
147
+ const validation = validateTurtle(turtle, shapesStore, shapeFiles, fixture.id);
148
+
149
+ if (validationMode === 'shacl-valid') {
150
+ if (validation.valid) {
151
+ printVerbose(` [${fixture.id}] SHACL valid (${validation.quadCount} quads)`, opts);
152
+ return { ...base, status: 'passed', validationDetails: validation };
153
+ } else {
154
+ const violations = validation.results
155
+ .filter((r) => r.severity === 'violation')
156
+ .map((r) => `${r.shape}.${r.property}: ${r.message}`)
157
+ .join('; ');
158
+ return {
159
+ ...base,
160
+ status: 'failed',
161
+ details: `SHACL violations: ${violations}`,
162
+ validationDetails: validation,
163
+ };
164
+ }
165
+ } else if (validationMode === 'exact-match') {
166
+ // Exact-match: for now, verify it parses and is SHACL-valid
167
+ // Full normalized triple-by-triple equivalence is deferred
168
+ if (validation.valid) {
169
+ printVerbose(
170
+ ` [${fixture.id}] exact-match: parsed & SHACL valid (full normalization deferred)`,
171
+ opts,
172
+ );
173
+ return { ...base, status: 'passed', validationDetails: validation };
174
+ } else {
175
+ const violations = validation.results
176
+ .filter((r) => r.severity === 'violation')
177
+ .map((r) => `${r.shape}.${r.property}: ${r.message}`)
178
+ .join('; ');
179
+ return {
180
+ ...base,
181
+ status: 'failed',
182
+ details: `SHACL violations (exact-match mode): ${violations}`,
183
+ validationDetails: validation,
184
+ };
185
+ }
186
+ } else {
187
+ return {
188
+ ...base,
189
+ status: 'error',
190
+ details: `Unknown validationMode: ${validationMode as string}`,
191
+ };
192
+ }
193
+ } else {
194
+ // ---- Negative fixture ----
195
+
196
+ if (turtle === '') {
197
+ // Empty turtle means the SDK should reject before serialization — PASS
198
+ printVerbose(` [${fixture.id}] negative: empty turtle (pre-serialization rejection)`, opts);
199
+ return { ...base, status: 'passed' };
200
+ }
201
+
202
+ // Non-empty turtle: validate and expect violations
203
+ const parseResult = parseTurtle(turtle);
204
+ if (!parseResult.success) {
205
+ // Parse failure on negative fixture = PASS (malformed is invalid)
206
+ printVerbose(` [${fixture.id}] negative: turtle parse error (expected)`, opts);
207
+ return { ...base, status: 'passed', details: 'Turtle parse error (expected for negative fixture)' };
208
+ }
209
+
210
+ const validation = validateTurtle(turtle, shapesStore, shapeFiles, fixture.id);
211
+
212
+ if (!validation.valid) {
213
+ // Violations found — PASS for negative fixture
214
+ const violations = validation.results
215
+ .filter((r) => r.severity === 'violation')
216
+ .map((r) => `${r.shape}.${r.property}: ${r.message}`)
217
+ .join('; ');
218
+ printVerbose(` [${fixture.id}] negative: SHACL violations found (expected): ${violations}`, opts);
219
+ return { ...base, status: 'passed', validationDetails: validation };
220
+ } else {
221
+ // No violations — FAIL for negative fixture
222
+ return {
223
+ ...base,
224
+ status: 'failed',
225
+ details: `Expected SHACL violations but data validated clean. Expected: ${fixture.shaclConstraintViolated ?? 'unspecified'}`,
226
+ validationDetails: validation,
227
+ };
228
+ }
229
+ }
230
+ } catch (err: unknown) {
231
+ const message = err instanceof Error ? err.message : String(err);
232
+ return {
233
+ ...base,
234
+ status: 'error',
235
+ details: `Unexpected error: ${message}`,
236
+ };
237
+ }
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Report generation
242
+ // ---------------------------------------------------------------------------
243
+
244
+ /**
245
+ * Build the aggregate suite report from individual fixture results.
246
+ */
247
+ function buildReport(
248
+ suitePath: string,
249
+ mode: string,
250
+ results: FixtureResult[],
251
+ ): SuiteReport {
252
+ const byDataType: SuiteReport['byDataType'] = {};
253
+
254
+ for (const r of results) {
255
+ if (!byDataType[r.dataType]) {
256
+ byDataType[r.dataType] = { total: 0, passed: 0, failed: 0, errors: 0 };
257
+ }
258
+ const group = byDataType[r.dataType];
259
+ group.total++;
260
+ if (r.status === 'passed') group.passed++;
261
+ else if (r.status === 'failed') group.failed++;
262
+ else group.errors++;
263
+ }
264
+
265
+ return {
266
+ suite: suitePath,
267
+ mode,
268
+ total: results.length,
269
+ passed: results.filter((r) => r.status === 'passed').length,
270
+ failed: results.filter((r) => r.status === 'failed').length,
271
+ errors: results.filter((r) => r.status === 'error').length,
272
+ results,
273
+ byDataType,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Print a human-readable report to stdout.
279
+ */
280
+ function printHumanReport(report: SuiteReport, opts: OutputOptions): void {
281
+ console.log('');
282
+ console.log('Cascade Protocol Conformance Test Suite');
283
+ console.log('========================================');
284
+ console.log(`Suite: ${report.suite}`);
285
+ console.log(`Mode: ${report.mode === 'self' ? 'self-conformance' : report.mode}`);
286
+ console.log(`Fixtures: ${report.total}`);
287
+ console.log('');
288
+ console.log('Running tests...');
289
+ console.log('');
290
+
291
+ // Group results by dataType, preserving insertion order
292
+ const dataTypes: string[] = [];
293
+ const grouped: Record<string, FixtureResult[]> = {};
294
+
295
+ for (const r of report.results) {
296
+ if (!grouped[r.dataType]) {
297
+ grouped[r.dataType] = [];
298
+ dataTypes.push(r.dataType);
299
+ }
300
+ grouped[r.dataType].push(r);
301
+ }
302
+
303
+ for (const dt of dataTypes) {
304
+ const fixtures = grouped[dt];
305
+ console.log(` ${dt} (${fixtures.length} fixtures)`);
306
+
307
+ for (const r of fixtures) {
308
+ const icon = r.status === 'passed' ? '\u2713' : r.status === 'failed' ? '\u2717' : '!';
309
+ const negativeTag = r.negative ? '[negative] ' : '';
310
+ const statusLine = ` ${icon} ${r.id}: ${negativeTag}${r.description}`;
311
+ console.log(statusLine);
312
+
313
+ if (r.status !== 'passed' && r.details) {
314
+ console.log(` ${r.details}`);
315
+ }
316
+
317
+ // In verbose mode, show validation details even for passing tests
318
+ if (opts.verbose && r.validationDetails) {
319
+ const vd = r.validationDetails;
320
+ console.log(` Quads: ${vd.quadCount}, Shapes: [${vd.shapesUsed.join(', ')}]`);
321
+ if (vd.results.length > 0) {
322
+ for (const issue of vd.results) {
323
+ console.log(` - [${issue.severity}] ${issue.shape}.${issue.property}: ${issue.message}`);
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ console.log('');
330
+ }
331
+
332
+ // Summary line
333
+ const parts: string[] = [];
334
+ parts.push(`${report.passed} passed`);
335
+ if (report.failed > 0) parts.push(`${report.failed} failed`);
336
+ else parts.push('0 failed');
337
+ if (report.errors > 0) parts.push(`${report.errors} errors`);
338
+ else parts.push('0 errors');
339
+
340
+ console.log(`Results: ${parts.join(', ')}`);
341
+
342
+ const exitCode = report.failed > 0 || report.errors > 0 ? 1 : 0;
343
+ console.log(`Exit code: ${exitCode}`);
344
+ }
345
+
346
+ /**
347
+ * Print the JSON report using the output library.
348
+ */
349
+ function printJsonReport(report: SuiteReport, opts: OutputOptions): void {
350
+ // Strip validationDetails from results for cleaner JSON output
351
+ const cleanResults = report.results.map((r) => {
352
+ const { validationDetails: _vd, ...rest } = r;
353
+ return rest;
354
+ });
355
+
356
+ printResult(
357
+ {
358
+ suite: report.suite,
359
+ mode: report.mode,
360
+ total: report.total,
361
+ passed: report.passed,
362
+ failed: report.failed,
363
+ errors: report.errors,
364
+ results: cleanResults,
365
+ byDataType: report.byDataType,
366
+ },
367
+ opts,
368
+ );
369
+ }
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // Command registration
373
+ // ---------------------------------------------------------------------------
374
+
375
+ export function registerConformanceCommand(program: Command): void {
376
+ const conformance = program
377
+ .command('conformance')
378
+ .description('Run conformance test suite');
379
+
380
+ conformance
381
+ .command('run')
382
+ .description('Execute conformance tests')
383
+ .requiredOption('--suite <fixtures-dir>', 'Path to test fixtures directory')
384
+ .option('--command <cmd>', 'External command to test against')
385
+ .option('--self', 'Run self-conformance tests')
386
+ .action(
387
+ async (options: {
388
+ suite: string;
389
+ command?: string;
390
+ self?: boolean;
391
+ }) => {
392
+ const globalOpts = program.opts() as OutputOptions;
393
+
394
+ if (!options.command && !options.self) {
395
+ printError('Either --command or --self must be specified', globalOpts);
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+
400
+ // External command mode: not yet supported
401
+ if (options.command) {
402
+ printError('External command mode not yet supported', globalOpts);
403
+ process.exitCode = 1;
404
+ return;
405
+ }
406
+
407
+ // Resolve suite directory
408
+ const suitePath = path.resolve(options.suite);
409
+ printVerbose(`Conformance suite: ${suitePath}`, globalOpts);
410
+ printVerbose('Running self-conformance tests', globalOpts);
411
+
412
+ try {
413
+ // Load fixtures
414
+ const fixtures = loadFixtures(suitePath);
415
+ printVerbose(`Loaded ${fixtures.length} fixture(s)`, globalOpts);
416
+
417
+ // Load SHACL shapes once
418
+ const { store: shapesStore, shapeFiles } = loadShapes();
419
+ printVerbose(`Loaded shapes: ${shapeFiles.join(', ')}`, globalOpts);
420
+
421
+ // Run each fixture
422
+ const results: FixtureResult[] = [];
423
+
424
+ for (const fixture of fixtures) {
425
+ const result = runSelfFixture(fixture, shapesStore, shapeFiles, globalOpts);
426
+ results.push(result);
427
+ }
428
+
429
+ // Build and output report
430
+ const report = buildReport(options.suite, 'self', results);
431
+
432
+ if (globalOpts.json) {
433
+ printJsonReport(report, globalOpts);
434
+ } else {
435
+ printHumanReport(report, globalOpts);
436
+ }
437
+
438
+ // Set exit code
439
+ process.exitCode = report.failed > 0 || report.errors > 0 ? 1 : 0;
440
+ } catch (err: unknown) {
441
+ const message = err instanceof Error ? err.message : String(err);
442
+ printError(`Conformance suite failed: ${message}`, globalOpts);
443
+ process.exitCode = 1;
444
+ }
445
+ },
446
+ );
447
+ }