@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.
- package/.dockerignore +7 -0
- package/.eslintrc.json +23 -0
- package/.prettierrc +7 -0
- package/DOCKER.md +36 -0
- package/Dockerfile +18 -0
- package/README.md +69 -0
- package/dist/commands/capabilities.d.ts +9 -0
- package/dist/commands/capabilities.d.ts.map +1 -0
- package/dist/commands/capabilities.js +194 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/conformance.d.ts +15 -0
- package/dist/commands/conformance.d.ts.map +1 -0
- package/dist/commands/conformance.js +348 -0
- package/dist/commands/conformance.js.map +1 -0
- package/dist/commands/convert.d.ts +21 -0
- package/dist/commands/convert.d.ts.map +1 -0
- package/dist/commands/convert.js +134 -0
- package/dist/commands/convert.js.map +1 -0
- package/dist/commands/pod/export.d.ts +8 -0
- package/dist/commands/pod/export.d.ts.map +1 -0
- package/dist/commands/pod/export.js +72 -0
- package/dist/commands/pod/export.js.map +1 -0
- package/dist/commands/pod/helpers.d.ts +79 -0
- package/dist/commands/pod/helpers.d.ts.map +1 -0
- package/dist/commands/pod/helpers.js +369 -0
- package/dist/commands/pod/helpers.js.map +1 -0
- package/dist/commands/pod/index.d.ts +20 -0
- package/dist/commands/pod/index.d.ts.map +1 -0
- package/dist/commands/pod/index.js +29 -0
- package/dist/commands/pod/index.js.map +1 -0
- package/dist/commands/pod/info.d.ts +9 -0
- package/dist/commands/pod/info.d.ts.map +1 -0
- package/dist/commands/pod/info.js +196 -0
- package/dist/commands/pod/info.js.map +1 -0
- package/dist/commands/pod/init.d.ts +9 -0
- package/dist/commands/pod/init.d.ts.map +1 -0
- package/dist/commands/pod/init.js +251 -0
- package/dist/commands/pod/init.js.map +1 -0
- package/dist/commands/pod/query.d.ts +9 -0
- package/dist/commands/pod/query.d.ts.map +1 -0
- package/dist/commands/pod/query.js +169 -0
- package/dist/commands/pod/query.js.map +1 -0
- package/dist/commands/pod 2.js +1017 -0
- package/dist/commands/pod.d.ts +28 -0
- package/dist/commands/pod.d.ts 2.map +1 -0
- package/dist/commands/pod.d.ts.map +1 -0
- package/dist/commands/pod.js +1031 -0
- package/dist/commands/pod.js 2.map +1 -0
- package/dist/commands/pod.js.map +1 -0
- package/dist/commands/serve.d.ts +33 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +74 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/validate.d.ts +18 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +275 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
- package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
- package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
- package/dist/lib/fhir-converter/converters-clinical.js +391 -0
- package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
- package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
- package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
- package/dist/lib/fhir-converter/converters-demographics.js +242 -0
- package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
- package/dist/lib/fhir-converter/index.d.ts +36 -0
- package/dist/lib/fhir-converter/index.d.ts.map +1 -0
- package/dist/lib/fhir-converter/index.js +187 -0
- package/dist/lib/fhir-converter/index.js.map +1 -0
- package/dist/lib/fhir-converter/types.d.ts +77 -0
- package/dist/lib/fhir-converter/types.d.ts.map +1 -0
- package/dist/lib/fhir-converter/types.js +236 -0
- package/dist/lib/fhir-converter/types.js.map +1 -0
- package/dist/lib/fhir-converter.d.ts +62 -0
- package/dist/lib/fhir-converter.d.ts.map +1 -0
- package/dist/lib/fhir-converter.js +1474 -0
- package/dist/lib/fhir-converter.js.map +1 -0
- package/dist/lib/mcp/audit.d.ts +24 -0
- package/dist/lib/mcp/audit.d.ts.map +1 -0
- package/dist/lib/mcp/audit.js +85 -0
- package/dist/lib/mcp/audit.js.map +1 -0
- package/dist/lib/mcp/server.d.ts +38 -0
- package/dist/lib/mcp/server.d.ts.map +1 -0
- package/dist/lib/mcp/server.js +172 -0
- package/dist/lib/mcp/server.js.map +1 -0
- package/dist/lib/mcp/tools.d.ts +47 -0
- package/dist/lib/mcp/tools.d.ts.map +1 -0
- package/dist/lib/mcp/tools.js +547 -0
- package/dist/lib/mcp/tools.js.map +1 -0
- package/dist/lib/output.d.ts +26 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +64 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/shacl-validator.d.ts +53 -0
- package/dist/lib/shacl-validator.d.ts.map +1 -0
- package/dist/lib/shacl-validator.js +245 -0
- package/dist/lib/shacl-validator.js.map +1 -0
- package/dist/lib/turtle-parser.d.ts +64 -0
- package/dist/lib/turtle-parser.d.ts.map +1 -0
- package/dist/lib/turtle-parser.js +236 -0
- package/dist/lib/turtle-parser.js.map +1 -0
- package/dist/shapes/checkup.shapes.ttl +1459 -0
- package/dist/shapes/clinical.shapes.ttl +1350 -0
- package/dist/shapes/clinical.ttl +1369 -0
- package/dist/shapes/core.shapes.ttl +450 -0
- package/dist/shapes/core.ttl +603 -0
- package/dist/shapes/coverage.shapes.ttl +214 -0
- package/dist/shapes/coverage.ttl +182 -0
- package/dist/shapes/health.shapes.ttl +697 -0
- package/dist/shapes/health.ttl +859 -0
- package/dist/shapes/pots.shapes.ttl +481 -0
- package/package.json +54 -0
- package/src/commands/capabilities.ts +235 -0
- package/src/commands/conformance.ts +447 -0
- package/src/commands/convert.ts +164 -0
- package/src/commands/pod/export.ts +85 -0
- package/src/commands/pod/helpers.ts +449 -0
- package/src/commands/pod/index.ts +32 -0
- package/src/commands/pod/info.ts +239 -0
- package/src/commands/pod/init.ts +273 -0
- package/src/commands/pod/query.ts +224 -0
- package/src/commands/serve.ts +92 -0
- package/src/commands/validate.ts +303 -0
- package/src/index.ts +58 -0
- package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
- package/src/lib/fhir-converter/converters-clinical.ts +446 -0
- package/src/lib/fhir-converter/converters-demographics.ts +270 -0
- package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
- package/src/lib/fhir-converter/index.ts +215 -0
- package/src/lib/fhir-converter/types.ts +318 -0
- package/src/lib/mcp/audit.ts +107 -0
- package/src/lib/mcp/server.ts +192 -0
- package/src/lib/mcp/tools.ts +668 -0
- package/src/lib/output.ts +76 -0
- package/src/lib/shacl-validator.ts +314 -0
- package/src/lib/turtle-parser.ts +277 -0
- package/src/shapes/checkup.shapes.ttl +1459 -0
- package/src/shapes/clinical.shapes.ttl +1350 -0
- package/src/shapes/clinical.ttl +1369 -0
- package/src/shapes/core.shapes.ttl +450 -0
- package/src/shapes/core.ttl +603 -0
- package/src/shapes/coverage.shapes.ttl +214 -0
- package/src/shapes/coverage.ttl +182 -0
- package/src/shapes/health.shapes.ttl +697 -0
- package/src/shapes/health.ttl +859 -0
- package/src/shapes/pots.shapes.ttl +481 -0
- package/test-fixtures/fhir-bundle-example.json +216 -0
- package/test-fixtures/fhir-medication-example.json +18 -0
- package/tests/cli.test.ts +126 -0
- package/tests/fhir-converter.test.ts +874 -0
- package/tests/mcp-server.test.ts +396 -0
- package/tests/pod.test.ts +400 -0
- 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
|
+
}
|